mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -9,16 +9,24 @@
|
|
|
9
9
|
* - DEFAULT_REFINE_THRESHOLD — seed length below which refinement
|
|
10
10
|
* is auto-suggested.
|
|
11
11
|
* - REQUIRED_SECTIONS — canonical section headings the body
|
|
12
|
-
* must carry
|
|
13
|
-
* /
|
|
12
|
+
* must carry itself. Story #4874: the
|
|
13
|
+
* `## Acceptance` / `## Verify` sections
|
|
14
|
+
* are deliberately NOT among them —
|
|
15
|
+
* those lists are the ticket's top-level
|
|
16
|
+
* machine contract and persist
|
|
17
|
+
* synthesizes their sections, exactly as
|
|
18
|
+
* the story-author prompt instructs.
|
|
14
19
|
* - rankDuplicateCandidates({ seed, openStories, maxResults })
|
|
15
20
|
* — Jaccard-overlap ranking of open
|
|
16
21
|
* Stories whose titles fuzzy-match
|
|
17
22
|
* the seed.
|
|
18
|
-
* - validateStoryBody(body)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* - validateStoryBody(body, contract)
|
|
24
|
+
* — schema-light shape check against the
|
|
25
|
+
* canonical Story-body contract: `##
|
|
26
|
+
* Goal` / `## Changes` present, no
|
|
27
|
+
* `Epic:` reference, and an
|
|
28
|
+
* acceptance/verify contract resolvable
|
|
29
|
+
* from the top-level arrays or the body.
|
|
22
30
|
* - buildContextEnvelope(opts) — assemble the context envelope the
|
|
23
31
|
* host LLM consumes.
|
|
24
32
|
*/
|
|
@@ -26,21 +34,25 @@
|
|
|
26
34
|
import { readFile } from 'node:fs/promises';
|
|
27
35
|
import path from 'node:path';
|
|
28
36
|
import { overlapScore, tokenize } from './duplicate-search.js';
|
|
37
|
+
import { parse as parseStoryBody } from './story-body/story-body.js';
|
|
29
38
|
|
|
30
39
|
export const DEFAULT_REFINE_THRESHOLD = 200;
|
|
31
40
|
export const DEFAULT_DUPLICATE_MAX_RESULTS = 5;
|
|
32
41
|
export const DEFAULT_DUPLICATE_MIN_SCORE = 0.15;
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The sections the authored body must carry itself. Story #4874 reconciled
|
|
45
|
+
* this list with the story-author prompt: the prompt tells the author to
|
|
46
|
+
* write `acceptance[]` / `verify[]` **once** at the ticket's top level and
|
|
47
|
+
* omit the matching body sections, so demanding those sections here made a
|
|
48
|
+
* prompt-faithful body unpersistable and cost a re-author round.
|
|
49
|
+
*/
|
|
50
|
+
export const REQUIRED_SECTIONS = ['Goal', 'Changes'];
|
|
51
|
+
|
|
52
|
+
/** The contract lists persist synthesizes into the body from the top level. */
|
|
53
|
+
const CONTRACT_FIELDS = /** @type {const} */ (['acceptance', 'verify']);
|
|
40
54
|
|
|
41
55
|
const EPIC_REF_PATTERN = /^\s*Epic:\s*#\d+/m;
|
|
42
|
-
const AC_HEADING_PATTERN = /^##\s+Acceptance Criteria\s*$/m;
|
|
43
|
-
const CHECKLIST_PATTERN = /^\s*-\s*\[\s?\]/m;
|
|
44
56
|
|
|
45
57
|
/**
|
|
46
58
|
* Rank open Stories by title-overlap with the seed. Reuses the same
|
|
@@ -115,54 +127,137 @@ export function shouldRefine({
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
130
|
+
* Collect the violations for one contract list (`acceptance` / `verify`).
|
|
131
|
+
*
|
|
132
|
+
* The list may be authored at the ticket's top level (the shape the
|
|
133
|
+
* story-author prompt prescribes) or written into the body as a section —
|
|
134
|
+
* either satisfies the contract, and the top level is preferred because
|
|
135
|
+
* persist synthesizes the section from it. Only two shapes are violations:
|
|
136
|
+
* the list is nowhere, or it is in both places and the two disagree (fail
|
|
137
|
+
* closed rather than guess which is authoritative, mirroring
|
|
138
|
+
* `plan-persist`'s `syncContractFieldFromTopLevel`).
|
|
139
|
+
*
|
|
140
|
+
* @param {'acceptance'|'verify'} field
|
|
141
|
+
* @param {unknown} bodyList The parsed body's section entries.
|
|
142
|
+
* @param {unknown} topLevel The ticket's top-level array.
|
|
143
|
+
* @returns {string[]}
|
|
144
|
+
*/
|
|
145
|
+
function collectContractErrors(field, bodyList, topLevel) {
|
|
146
|
+
const inBody = (Array.isArray(bodyList) ? bodyList : []).map(String);
|
|
147
|
+
const inTicket = (Array.isArray(topLevel) ? topLevel : []).map(String);
|
|
148
|
+
if (inBody.length === 0 && inTicket.length === 0) {
|
|
149
|
+
return [
|
|
150
|
+
`${field} must list at least one entry — author it as the ticket's top-level ${field}[] array; persist synthesizes the "## ${field[0].toUpperCase()}${field.slice(1)}" section from it`,
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
inBody.length > 0 &&
|
|
155
|
+
inTicket.length > 0 &&
|
|
156
|
+
(inBody.length !== inTicket.length ||
|
|
157
|
+
inBody.some((v, i) => v !== inTicket[i]))
|
|
158
|
+
) {
|
|
159
|
+
return [
|
|
160
|
+
`${field} disagrees between the body section and the top-level ${field}[] array — author it once, at the top level`,
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Schema-light validator for a standalone-Story body, stated against the
|
|
168
|
+
* same authoring shape the story-author prompt prescribes (Story #4874).
|
|
169
|
+
*
|
|
170
|
+
* It parses the body with the canonical Story-body parser and asserts:
|
|
171
|
+
* a non-empty `## Goal`, at least one `## Changes` entry, no leaking
|
|
172
|
+
* `Epic: #N` reference (the standalone contract), and an acceptance +
|
|
173
|
+
* verify contract resolvable from the ticket's top-level arrays **or** the
|
|
174
|
+
* body's own sections. It deliberately does not demand the `## Acceptance`
|
|
175
|
+
* / `## Verify` sections of the author — persist synthesizes them.
|
|
124
176
|
*
|
|
125
177
|
* @param {string} body
|
|
178
|
+
* @param {{ acceptance?: string[], verify?: string[] }} [contract]
|
|
179
|
+
* The ticket's top-level contract arrays, when the caller has them.
|
|
126
180
|
* @returns {{ ok:boolean, errors:string[] }}
|
|
127
181
|
*/
|
|
128
|
-
export function validateStoryBody(body) {
|
|
129
|
-
const errors = [];
|
|
182
|
+
export function validateStoryBody(body, contract = {}) {
|
|
130
183
|
if (typeof body !== 'string' || body.trim().length === 0) {
|
|
131
184
|
return { ok: false, errors: ['body is empty'] };
|
|
132
185
|
}
|
|
133
186
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = parseStoryBody(body).body;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
errors: [`body is not a parseable Story body: ${err.message}`],
|
|
194
|
+
};
|
|
139
195
|
}
|
|
140
196
|
|
|
197
|
+
const errors = [];
|
|
198
|
+
if (typeof parsed.goal !== 'string' || parsed.goal.trim() === '') {
|
|
199
|
+
errors.push('missing required section: "## Goal"');
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(parsed.changes) || parsed.changes.length === 0) {
|
|
202
|
+
errors.push('missing required section: "## Changes"');
|
|
203
|
+
}
|
|
141
204
|
if (EPIC_REF_PATTERN.test(body)) {
|
|
142
205
|
errors.push(
|
|
143
206
|
'body contains an "Epic: #N" reference — standalone Stories must not link to an Epic',
|
|
144
207
|
);
|
|
145
208
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (acStart !== -1) {
|
|
151
|
-
const rest = body.slice(acStart);
|
|
152
|
-
const nextHeading = rest.slice(1).search(/^##\s+/m);
|
|
153
|
-
const acBlock = nextHeading === -1 ? rest : rest.slice(0, nextHeading + 1);
|
|
154
|
-
if (!CHECKLIST_PATTERN.test(acBlock)) {
|
|
155
|
-
errors.push(
|
|
156
|
-
'Acceptance Criteria section has no unchecked checklist items (`- [ ] ...`)',
|
|
157
|
-
);
|
|
158
|
-
}
|
|
209
|
+
for (const field of CONTRACT_FIELDS) {
|
|
210
|
+
errors.push(
|
|
211
|
+
...collectContractErrors(field, parsed[field], contract?.[field]),
|
|
212
|
+
);
|
|
159
213
|
}
|
|
160
214
|
|
|
161
215
|
return { ok: errors.length === 0, errors };
|
|
162
216
|
}
|
|
163
217
|
|
|
164
|
-
|
|
165
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Render the canonical `## Acceptance` / `## Verify` sections into a body
|
|
220
|
+
* that omits them, from the ticket's top-level arrays (Story #4874).
|
|
221
|
+
*
|
|
222
|
+
* This is the synthesis the story-author prompt promises: the author writes
|
|
223
|
+
* each list once at top level, and the persisted GitHub issue still reads as
|
|
224
|
+
* a complete executable document. A body that already carries a section is
|
|
225
|
+
* returned untouched — `validateStoryBody` has already refused the case
|
|
226
|
+
* where the two disagree.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} body
|
|
229
|
+
* @param {{ acceptance?: string[], verify?: string[] }} [contract]
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
export function synthesizeContractSections(body, contract = {}) {
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = parseStoryBody(body).body;
|
|
236
|
+
} catch {
|
|
237
|
+
return body;
|
|
238
|
+
}
|
|
239
|
+
const blocks = [];
|
|
240
|
+
if (
|
|
241
|
+
(parsed.acceptance ?? []).length === 0 &&
|
|
242
|
+
Array.isArray(contract.acceptance) &&
|
|
243
|
+
contract.acceptance.length > 0
|
|
244
|
+
) {
|
|
245
|
+
const items = contract.acceptance
|
|
246
|
+
.map((a, i) => `- [ ] AC-${i + 1}: ${a}`)
|
|
247
|
+
.join('\n');
|
|
248
|
+
blocks.push(`## Acceptance\n${items}`);
|
|
249
|
+
}
|
|
250
|
+
if (
|
|
251
|
+
(parsed.verify ?? []).length === 0 &&
|
|
252
|
+
Array.isArray(contract.verify) &&
|
|
253
|
+
contract.verify.length > 0
|
|
254
|
+
) {
|
|
255
|
+
blocks.push(
|
|
256
|
+
`## Verify\n${contract.verify.map((v) => `- ${v}`).join('\n')}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (blocks.length === 0) return body;
|
|
260
|
+
return `${body.trimEnd()}\n\n${blocks.join('\n\n')}\n`;
|
|
166
261
|
}
|
|
167
262
|
|
|
168
263
|
/**
|
|
@@ -113,9 +113,9 @@ You MUST respond ONLY with a valid JSON array of objects. No prose, no markdown
|
|
|
113
113
|
|
|
114
114
|
The \`acceptance[]\` and \`verify[]\` arrays live at the **top level** of the Story ticket object — that is the machine contract the validator reads. Author each list **once, at top level**, and **omit** the \`## Acceptance\` / \`## Verify\` sections from the authored \`body\` string: persist syncs the top-level arrays into those sections so the GitHub issue stays a complete executable document. The validator resolves both fields from the top level, so an omitted section is the expected shape, not a violation.
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Never hand-mirror either list into the \`body\` — there is no step that asks you to. Persist fails closed on a body section that disagrees with its top-level array rather than guessing which list is authoritative, so writing one anyway only adds a way to be wrong. Do **not** invent a second criteria list inside \`## Spec\`, and do not author a separate Acceptance Spec / PRD artifact.
|
|
117
117
|
|
|
118
|
-
The
|
|
118
|
+
The **persisted** \`body\` renders these markdown sections (in order) — you author every one of them except \`## Acceptance\` / \`## Verify\`, which persist synthesizes from the top-level arrays:
|
|
119
119
|
|
|
120
120
|
## Goal
|
|
121
121
|
<one sentence — why this Story exists>
|
|
@@ -130,11 +130,11 @@ The serialized \`body\` string renders these markdown sections (in order):
|
|
|
130
130
|
- {"path": "<file path>", "assumption": "creates" | "refactors-existing" | "deletes"}
|
|
131
131
|
- ...
|
|
132
132
|
|
|
133
|
-
## Acceptance
|
|
133
|
+
## Acceptance <-- synthesized by persist from acceptance[]; do not author
|
|
134
134
|
- [ ] <testable, observable criterion>
|
|
135
135
|
- ...
|
|
136
136
|
|
|
137
|
-
## Verify
|
|
137
|
+
## Verify <-- synthesized by persist from verify[]; do not author
|
|
138
138
|
- <exact command or test path> (<tier>)
|
|
139
139
|
- ...
|
|
140
140
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract every test-running entrypoint shares: the `node --test` flag
|
|
3
|
+
* set, and the per-tier preflight.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a module and not two literals (Story #4936)
|
|
6
|
+
*
|
|
7
|
+
* The repository has two full-tier runners — `run-tests.js` (`npm test`) and
|
|
8
|
+
* `run-coverage.js` (`npm run test:coverage`, the *required* CI job). When
|
|
9
|
+
* they disagree about a `node --test` flag, they disagree about whether a
|
|
10
|
+
* test can execute at all: `--experimental-test-module-mocks` decides whether
|
|
11
|
+
* `t.mock.module` works, so a divergence makes a suite pass under one runner
|
|
12
|
+
* and fail under the other with no source defect. `FULL_TIER_GLOBS` in
|
|
13
|
+
* [`test-tiers.js`](test-tiers.js) already solved the sibling problem for
|
|
14
|
+
* *which files* run; this module is the same fix for *how they are run*.
|
|
15
|
+
* Both runners import from here; neither restates a literal.
|
|
16
|
+
*
|
|
17
|
+
* ## The preflight is invoked, never hooked
|
|
18
|
+
*
|
|
19
|
+
* `.npmrc` sets `ignore-scripts=true` as deliberate defence against malicious
|
|
20
|
+
* postinstall hooks (CWE-1357). That setting is correct and stays — but it
|
|
21
|
+
* suppresses every `pre*` / `post*` lifecycle script for `npm run` as well as
|
|
22
|
+
* for installs, so a `pretest` entry in `package.json` never fires. The
|
|
23
|
+
* repository carried `pretest`, `pretest:quick` and `pretest:integration`
|
|
24
|
+
* entries that therefore ran for no tier at all, and Story #4922 worked
|
|
25
|
+
* around it for the coverage tier alone by having CI name `pretest:coverage`
|
|
26
|
+
* explicitly. `runTierPreflight` closes the gap for every tier by moving the
|
|
27
|
+
* invocation into the runners, where it executes under `npm test`,
|
|
28
|
+
* `npm run test:coverage`, and a bare `node .agents/scripts/run-tests.js`
|
|
29
|
+
* alike — no lifecycle hook involved.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import process from 'node:process';
|
|
36
|
+
|
|
37
|
+
/** Minimum and maximum bounds for `--test-concurrency`. */
|
|
38
|
+
export const TEST_CONCURRENCY_MIN = 1;
|
|
39
|
+
export const TEST_CONCURRENCY_MAX = 16;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the `--test-concurrency` value for the current host.
|
|
43
|
+
*
|
|
44
|
+
* Uses `os.availableParallelism()` (Node ≥18.14 / ≥20.0) clamped to the
|
|
45
|
+
* range `[TEST_CONCURRENCY_MIN, TEST_CONCURRENCY_MAX]`. The `parallelism`
|
|
46
|
+
* parameter is injected in tests so the clamping logic is verifiable
|
|
47
|
+
* without touching the OS.
|
|
48
|
+
*
|
|
49
|
+
* @param {number} [parallelism] - defaults to `os.availableParallelism()`
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
export function resolveTestConcurrency(
|
|
53
|
+
parallelism = os.availableParallelism(),
|
|
54
|
+
) {
|
|
55
|
+
return Math.min(
|
|
56
|
+
TEST_CONCURRENCY_MAX,
|
|
57
|
+
Math.max(TEST_CONCURRENCY_MIN, parallelism),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fixed `node --test` flags applied to every spawn of every runner.
|
|
63
|
+
*
|
|
64
|
+
* `--test-concurrency` is derived at startup from the host's available
|
|
65
|
+
* parallelism so the value suits the machine running the suite rather than
|
|
66
|
+
* being pinned to the historical constant of 8.
|
|
67
|
+
*
|
|
68
|
+
* This is the **single** declaration. A flag added here reaches both runners;
|
|
69
|
+
* a flag added to one runner's argv builder instead is caught by the
|
|
70
|
+
* flag-set equality assertion in `tests/scripts/run-coverage.test.js`.
|
|
71
|
+
*/
|
|
72
|
+
export const TEST_RUNNER_FLAGS = Object.freeze([
|
|
73
|
+
'--experimental-test-module-mocks',
|
|
74
|
+
'--test',
|
|
75
|
+
`--test-concurrency=${resolveTestConcurrency()}`,
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Preflight scripts per tier, repo-relative. These are the definitions that
|
|
80
|
+
* used to live in `package.json` as inert `pretest*` entries.
|
|
81
|
+
*
|
|
82
|
+
* `full` carries the skills validator on top of the state-probe wrapper
|
|
83
|
+
* because the full tier is the release-shaped run (and the surface the
|
|
84
|
+
* coverage tier measures); `quick` and `integration` run the state probe
|
|
85
|
+
* only, matching the tiers' historical `pretest:quick` / `pretest:integration`
|
|
86
|
+
* definitions. The coverage runner runs the full tier, so it shares `full`.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately not exported: a second importer would be a second place to
|
|
89
|
+
* read the tier→preflight mapping from, and tests assert the mapping through
|
|
90
|
+
* the spawns `runTierPreflight` actually issues — which is the thing that
|
|
91
|
+
* has to be true.
|
|
92
|
+
*/
|
|
93
|
+
const TIER_PREFLIGHT_SCRIPTS = Object.freeze({
|
|
94
|
+
full: Object.freeze([
|
|
95
|
+
'.agents/scripts/test-wrapper.js',
|
|
96
|
+
'.agents/scripts/validate-skills.js',
|
|
97
|
+
]),
|
|
98
|
+
quick: Object.freeze(['.agents/scripts/test-wrapper.js']),
|
|
99
|
+
integration: Object.freeze(['.agents/scripts/test-wrapper.js']),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run the preflight for a tier, in order, stopping at the first failure.
|
|
104
|
+
*
|
|
105
|
+
* Mirrors npm's own `pre<script>` semantics — a failed preflight aborts
|
|
106
|
+
* before the test runner is spawned and propagates its exit code (the
|
|
107
|
+
* wrapper reserves 2 for "preflight refused") — except that it actually
|
|
108
|
+
* executes, which the npm hook does not under `ignore-scripts=true`.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} [opts]
|
|
111
|
+
* @param {'full' | 'quick' | 'integration'} [opts.tier]
|
|
112
|
+
* @param {string} [opts.repoRoot] Absolute repository root.
|
|
113
|
+
* @param {typeof spawnSync} [opts.spawn] Injected in tests.
|
|
114
|
+
* @param {string} [opts.execPath] Node binary to spawn; injected in tests.
|
|
115
|
+
* @returns {number} 0 when every preflight script passed, else the first
|
|
116
|
+
* non-zero exit code.
|
|
117
|
+
*/
|
|
118
|
+
export function runTierPreflight({
|
|
119
|
+
tier = 'full',
|
|
120
|
+
repoRoot = process.cwd(),
|
|
121
|
+
spawn = spawnSync,
|
|
122
|
+
execPath = process.execPath,
|
|
123
|
+
} = {}) {
|
|
124
|
+
for (const script of TIER_PREFLIGHT_SCRIPTS[tier] ?? []) {
|
|
125
|
+
const run = spawn(execPath, [path.join(repoRoot, script)], {
|
|
126
|
+
cwd: repoRoot,
|
|
127
|
+
stdio: 'inherit',
|
|
128
|
+
});
|
|
129
|
+
if (run.error) throw run.error;
|
|
130
|
+
const status = run.status ?? 1;
|
|
131
|
+
if (status !== 0) return status;
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
@@ -35,7 +35,9 @@ const matchesIntegration = picomatch(INTEGRATION_INCLUDE, { dot: true });
|
|
|
35
35
|
* same way (Story #4195). Without each root here, both the quick /
|
|
36
36
|
* integration walk and the full-tier glob set miss the colocated tests,
|
|
37
37
|
* leaving that coverage dark in `npm test`. The matching full-tier globs
|
|
38
|
-
* live in `FULL_TIER_GLOBS
|
|
38
|
+
* live in the exported `FULL_TIER_GLOBS` — every full-tier runner
|
|
39
|
+
* (`run-tests.js`, `run-coverage.js`) MUST consume that constant rather than
|
|
40
|
+
* restate a glob literal, or a runner silently walks a narrower surface.
|
|
39
41
|
*/
|
|
40
42
|
const TEST_WALK_ROOTS = ['tests', 'lib', '.agents/scripts'];
|
|
41
43
|
|
|
@@ -44,8 +46,15 @@ const TEST_WALK_ROOTS = ['tests', 'lib', '.agents/scripts'];
|
|
|
44
46
|
* The `tests` glob is a flat recursive sweep; the `lib` and `.agents/scripts`
|
|
45
47
|
* globs are scoped to `__tests__` subtrees so they only match colocated
|
|
46
48
|
* tests, never the shipped source modules themselves.
|
|
49
|
+
*
|
|
50
|
+
* Exported because the full tier has two runners, not one: `run-tests.js`
|
|
51
|
+
* (via `listTestFilesForTier`) and `run-coverage.js`. Story #4922 — the
|
|
52
|
+
* coverage runner used to restate `tests/**` on its own, so the 47 colocated
|
|
53
|
+
* `__tests__` files ran under `npm test` but were absent from the measured
|
|
54
|
+
* surface, leaving the coverage and CRAP numbers computed over code the
|
|
55
|
+
* measuring run never executed. Consume this constant; never restate a glob.
|
|
47
56
|
*/
|
|
48
|
-
const FULL_TIER_GLOBS = [
|
|
57
|
+
export const FULL_TIER_GLOBS = [
|
|
49
58
|
'tests/**/*.test.js',
|
|
50
59
|
'lib/**/__tests__/**/*.test.js',
|
|
51
60
|
'.agents/scripts/**/__tests__/**/*.test.js',
|
|
@@ -235,7 +235,7 @@ export function sliceTicketBodyForDelivery(body) {
|
|
|
235
235
|
// survive verbatim.
|
|
236
236
|
const techLoc = locate(working, 'techSpec');
|
|
237
237
|
let techRegion = null;
|
|
238
|
-
const PLACEHOLDER = '
|
|
238
|
+
const PLACEHOLDER = '\u0000MANDREL_TECH_SPEC_PLACEHOLDER\u0000';
|
|
239
239
|
if (techLoc) {
|
|
240
240
|
const { end } = descriptor('techSpec');
|
|
241
241
|
techRegion = working.slice(techLoc.startIdx, techLoc.endIdx + end.length);
|
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
* and would leak otherwise.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The one bound every independent-write fan-out over the GitHub API uses
|
|
18
|
+
* (Story #4952 raised those loops off serial; Story #4961 made this the single
|
|
19
|
+
* owner of the number they share). Imported by the `/plan` context gathers,
|
|
20
|
+
* the persist checkpoint fan-out, the `agent::ready` flips and the supersede
|
|
21
|
+
* close loop, so re-tuning the policy is one edit rather than six.
|
|
22
|
+
*
|
|
23
|
+
* **Why bounded and not unbounded.** Every unit in those fan-outs is its own
|
|
24
|
+
* API round-trip, so an unbounded map over an N-Story plan dispatches N writes
|
|
25
|
+
* at once — and GitHub answers a burst with a secondary rate limit rather than
|
|
26
|
+
* with throughput. The goal is overlapping unrelated waits, not saturating the
|
|
27
|
+
* API, and four is enough to collapse the latency the serial loops paid while
|
|
28
|
+
* staying well under the burst threshold. Callers whose ordering is
|
|
29
|
+
* load-bearing (`createStoryIssues`) stay serial instead of importing this.
|
|
30
|
+
*/
|
|
31
|
+
export const FANOUT_CONCURRENCY = 4;
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* @template T, R
|
|
18
35
|
* @param {ReadonlyArray<T>} items
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-id-list — expand a Story-id list that may contain dash ranges.
|
|
3
|
+
*
|
|
4
|
+
* Operators name a contiguous span of Stories the way they read one — as a
|
|
5
|
+
* range: `/deliver 4922 - 4926`. Enumerating it by hand is the kind of
|
|
6
|
+
* transcription step that silently drops or invents an id, so the range is a
|
|
7
|
+
* first-class shape of every delivery id list rather than something the host
|
|
8
|
+
* expands from prose.
|
|
9
|
+
*
|
|
10
|
+
* Accepted tokens, comma-separated:
|
|
11
|
+
* - a single id, with an optional `#` — `4922`, `#4922`
|
|
12
|
+
* - an inclusive range — `4922-4926`, `4922 - 4926`, `#4922-#4926`
|
|
13
|
+
* (hyphen-minus, en dash, or em dash; whitespace around it is fine)
|
|
14
|
+
*
|
|
15
|
+
* Everything else is a hard error, never a silent drop: a wrong id list
|
|
16
|
+
* co-dispatches against the wrong graph, so it must fail where it is typed.
|
|
17
|
+
* Two range-specific guards exist for the same reason — a backwards range is
|
|
18
|
+
* refused rather than expanded to nothing, and a span above `MAX_RANGE_SPAN`
|
|
19
|
+
* is refused rather than resolving thousands of issues off a typo.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Inclusive-span ceiling for a single range token. Generous against any real
|
|
24
|
+
* plan run (a handful of Stories) and tight enough that `1-4926` is caught as
|
|
25
|
+
* the typo it is rather than fanning out into a live resolution sweep.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately module-private: the cap is a published contract
|
|
28
|
+
* (`helpers/deliver-reference.md` § Ranges), so a test that imported it could
|
|
29
|
+
* not notice the number silently moving out from under the doc.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_RANGE_SPAN = 50;
|
|
32
|
+
|
|
33
|
+
/** Hyphen-minus, en dash, em dash — whichever the operator's keyboard emits. */
|
|
34
|
+
const DASH = '[-–—]';
|
|
35
|
+
const SINGLE_RE = /^#?(\d+)$/;
|
|
36
|
+
const RANGE_RE = new RegExp(`^#?(\\d+)\\s*${DASH}\\s*#?(\\d+)$`);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a comma-separated Story-id list, expanding any `A-B` range token.
|
|
40
|
+
*
|
|
41
|
+
* Absent or empty input is not an error here — it yields an empty list, and
|
|
42
|
+
* the caller decides whether that is a usage error (`--ids`) or a legitimate
|
|
43
|
+
* empty set (`--done`).
|
|
44
|
+
*
|
|
45
|
+
* @param {string|undefined|null} raw
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @param {string} [options.flag] Flag name, for the error message.
|
|
48
|
+
* @param {string} [options.prefix] Message prefix, for the caller's log tag.
|
|
49
|
+
* @param {number} [options.maxSpan] Inclusive-span ceiling per range token.
|
|
50
|
+
* @returns {{ ids: number[]|null, error: string|null }}
|
|
51
|
+
*/
|
|
52
|
+
export function expandIdList(raw, options = {}) {
|
|
53
|
+
const { flag = '--ids', prefix = '', maxSpan = MAX_RANGE_SPAN } = options;
|
|
54
|
+
const fail = (message) => ({ ids: null, error: `${prefix}${message}` });
|
|
55
|
+
|
|
56
|
+
const ids = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const push = (n) => {
|
|
59
|
+
if (seen.has(n)) return;
|
|
60
|
+
seen.add(n);
|
|
61
|
+
ids.push(n);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const token of String(raw ?? '').split(',')) {
|
|
65
|
+
const trimmed = token.trim();
|
|
66
|
+
if (trimmed === '') continue;
|
|
67
|
+
|
|
68
|
+
const range = RANGE_RE.exec(trimmed);
|
|
69
|
+
if (range) {
|
|
70
|
+
const start = Number(range[1]);
|
|
71
|
+
const end = Number(range[2]);
|
|
72
|
+
if (start <= 0 || end <= 0) {
|
|
73
|
+
return fail(
|
|
74
|
+
`${flag} range "${trimmed}" must use positive issue numbers.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (end < start) {
|
|
78
|
+
return fail(
|
|
79
|
+
`${flag} range "${trimmed}" runs backwards — write it low-to-high (e.g. 4922-4926).`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const span = end - start + 1;
|
|
83
|
+
if (span > maxSpan) {
|
|
84
|
+
return fail(
|
|
85
|
+
`${flag} range "${trimmed}" spans ${span} ids, above the ${maxSpan}-id cap. Narrow it, or list the ids.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
for (let n = start; n <= end; n++) push(n);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const single = SINGLE_RE.exec(trimmed);
|
|
93
|
+
const n = single ? Number(single[1]) : Number.NaN;
|
|
94
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
95
|
+
return fail(
|
|
96
|
+
`${flag} must be a comma-separated list of positive issue numbers or A-B ranges (got "${trimmed}").`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
push(n);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ids, error: null };
|
|
103
|
+
}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* `sleep` is exported as the cancellable delay primitive used internally;
|
|
14
14
|
* callers that already have their own cadence (e.g. a continuous ticker)
|
|
15
15
|
* can import it directly.
|
|
16
|
+
*
|
|
17
|
+
* `sleepFn` is the inter-tick delay seam (Story #4873). A caller that already
|
|
18
|
+
* owns a delay seam of its own — the close-and-land merge wait, whose suite
|
|
19
|
+
* must never actually sleep out a 30s poll interval — passes it here so
|
|
20
|
+
* adopting this primitive does not cost it that seam. It is called as
|
|
21
|
+
* `sleepFn(intervalMs, signal)`; a one-argument stub simply ignores the
|
|
22
|
+
* signal, and the default is the cancellable {@link sleep} below.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -23,11 +30,20 @@
|
|
|
23
30
|
* timeoutMs?: number,
|
|
24
31
|
* signal?: AbortSignal,
|
|
25
32
|
* logger?: { warn?: Function },
|
|
33
|
+
* sleepFn?: (ms: number, signal?: AbortSignal) => Promise<void>,
|
|
26
34
|
* }} opts
|
|
27
35
|
* @returns {Promise<any | undefined>}
|
|
28
36
|
*/
|
|
29
37
|
export async function pollUntil(opts) {
|
|
30
|
-
const {
|
|
38
|
+
const {
|
|
39
|
+
fn,
|
|
40
|
+
predicate,
|
|
41
|
+
intervalMs,
|
|
42
|
+
timeoutMs,
|
|
43
|
+
signal,
|
|
44
|
+
logger,
|
|
45
|
+
sleepFn = sleep,
|
|
46
|
+
} = opts;
|
|
31
47
|
if (typeof fn !== 'function') throw new TypeError('pollUntil: fn required');
|
|
32
48
|
if (typeof predicate !== 'function') {
|
|
33
49
|
throw new TypeError('pollUntil: predicate required');
|
|
@@ -52,7 +68,7 @@ export async function pollUntil(opts) {
|
|
|
52
68
|
if (deadline !== null && Date.now() >= deadline) {
|
|
53
69
|
throw new Error(`pollUntil: timed out after ${timeoutMs}ms`);
|
|
54
70
|
}
|
|
55
|
-
await
|
|
71
|
+
await sleepFn(intervalMs, signal);
|
|
56
72
|
}
|
|
57
73
|
return undefined;
|
|
58
74
|
}
|