mandrel 1.87.0 → 1.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +63 -16
- package/.agents/docs/configuration.md +5 -3
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +16 -4
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/config/explain.js +4 -0
- package/.agents/scripts/lib/config/runners.js +21 -2
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +193 -44
- package/.agents/workflows/helpers/deliver-epic.md +128 -39
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -283
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Story #3845 / Epic #3823. The audit-results graduator
|
|
5
5
|
* (`audit-results-graduator.js`) and the code-review graduator
|
|
6
|
-
* (`code-review-graduator.js`) duplicated ~90% of their mechanism: a
|
|
6
|
+
* (the since-retired `code-review-graduator.js`) duplicated ~90% of their mechanism: a
|
|
7
7
|
* `spawn`-based child runner, the `git cat-file` path probe, the
|
|
8
8
|
* `gh search issues` idempotency probe, the `gh issue create` filer, the
|
|
9
9
|
* `isAutoFileEnabled` toggle reader, and the route → probe → file
|
|
@@ -14,20 +14,111 @@
|
|
|
14
14
|
* The third `runGh` spawn copy in `prior-feedback-fetcher.js` is also
|
|
15
15
|
* collapsed onto the single `runChild` helper here.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
17
|
+
* Story #4415 / Epic #4406 hardens the walk so it is bounded and
|
|
18
|
+
* replay-safe — the shared mechanism the retro auto-filer will reuse:
|
|
19
|
+
*
|
|
20
|
+
* - **Content-hash idempotency markers.** Follow-up markers derive from
|
|
21
|
+
* a `category|path|title` digest (`contentFingerprint`) instead of a
|
|
22
|
+
* `(epicId, parse-index)` ordinal, so a finding keeps its marker when
|
|
23
|
+
* sibling findings are added, removed, or reordered in the source
|
|
24
|
+
* comment, and two distinct findings never collide.
|
|
25
|
+
* - **Legacy-marker recognition.** The idempotency probe also checks the
|
|
26
|
+
* legacy `(epicId, parse-index)` marker so findings filed before the
|
|
27
|
+
* fingerprint cutover are not re-filed.
|
|
28
|
+
* - **Bounded spawns.** `runChild` enforces a caller-overridable timeout
|
|
29
|
+
* (default 30000 ms) and kills a child that overruns instead of
|
|
30
|
+
* hanging finalize forever.
|
|
31
|
+
* - **Probe-error vs confirmed-missing.** A `git cat-file` spawn failure
|
|
32
|
+
* (or timeout) records the finding as skipped `probe-error`, not the
|
|
33
|
+
* confirmed-missing `file-removed`.
|
|
34
|
+
* - **Per-run filing cap.** `graduate()` stops filing once
|
|
35
|
+
* `maxFilingsPerRun` issues are created and records the excess as
|
|
36
|
+
* skipped `cap-reached`.
|
|
37
|
+
* - **Pre-parsed / path-less seam.** `graduate()` accepts a pre-parsed
|
|
38
|
+
* `findings` array (bypassing structured-comment parsing), and a
|
|
39
|
+
* path-less finding skips the path-exists gate instead of being
|
|
40
|
+
* misclassified `file-removed` — the seam the retro auto-filer
|
|
41
|
+
* consumes.
|
|
42
|
+
* - **Durable cross-repo deferral.** Cross-repo-deferred findings are
|
|
43
|
+
* upserted into a structured comment on the Epic instead of only a
|
|
44
|
+
* log line.
|
|
22
45
|
*/
|
|
23
46
|
|
|
24
47
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
25
49
|
|
|
26
50
|
import { classifyPathSource as defaultClassifier } from '../observability/source-classifier.js';
|
|
51
|
+
import {
|
|
52
|
+
structuredCommentMarker,
|
|
53
|
+
upsertStructuredComment,
|
|
54
|
+
} from '../orchestration/ticketing.js';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The single structured-comment marker for the unified `verification-results`
|
|
58
|
+
* findings contract (Story #4411, Epic #4405). It replaces the two retired
|
|
59
|
+
* per-graduator markers (the former code-review and audit-results
|
|
60
|
+
* structured-comment markers). Both feedback-loop graduators
|
|
61
|
+
* search the Epic's comments for this one marker so they file follow-ups
|
|
62
|
+
* from the same unified source comment that `runCodeReview` upserts (comment
|
|
63
|
+
* type `verification-results`). Derived from the canonical
|
|
64
|
+
* `structuredCommentMarker` builder so the read-side marker stays byte-stable
|
|
65
|
+
* with the write side rather than being hand-copied.
|
|
66
|
+
*/
|
|
67
|
+
export const VERIFICATION_RESULTS_MARKER = structuredCommentMarker(
|
|
68
|
+
'verification-results',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The single "no source comment" skip reason for the unified contract. Both
|
|
73
|
+
* graduators surface this reason when the Epic carries no
|
|
74
|
+
* `verification-results` comment, replacing the two retired
|
|
75
|
+
* `no-code-review-comment` / `no-audit-results-comment` reasons.
|
|
76
|
+
*/
|
|
77
|
+
export const NO_VERIFICATION_RESULTS_COMMENT_REASON =
|
|
78
|
+
'no-verification-results-comment';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default child-process timeout. A hung `gh`/`git` spawn previously blocked
|
|
82
|
+
* finalize indefinitely; the walk now caps every spawn at this bound unless
|
|
83
|
+
* a caller overrides it.
|
|
84
|
+
*/
|
|
85
|
+
export const DEFAULT_RUN_CHILD_TIMEOUT_MS = 30000;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Default per-run filing cap. `graduate()` files at most this many follow-up
|
|
89
|
+
* issues per invocation; the remainder is recorded as skipped `cap-reached`.
|
|
90
|
+
*/
|
|
91
|
+
export const DEFAULT_MAX_FILINGS_PER_RUN = 20;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Structured-comment type used to durably persist cross-repo-deferred
|
|
95
|
+
* findings on the Epic. Registered in `STRUCTURED_COMMENT_TYPES`.
|
|
96
|
+
*/
|
|
97
|
+
export const CROSS_REPO_DEFERRED_COMMENT_TYPE = 'cross-repo-deferred';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compute a stable content fingerprint for a finding from its
|
|
101
|
+
* `category|path|title` triple. Pure — the digest depends only on the
|
|
102
|
+
* finding content, never on its position in the source comment, so the
|
|
103
|
+
* marker survives sibling insert/remove/reorder churn. Distinct triples
|
|
104
|
+
* yield distinct digests (SHA-256, truncated to 16 hex chars for a compact
|
|
105
|
+
* marker that still has a negligible collision probability across a single
|
|
106
|
+
* Epic's finding set).
|
|
107
|
+
*
|
|
108
|
+
* @param {{ category?: unknown, path?: unknown, title?: unknown }} parts
|
|
109
|
+
* @returns {string} 16-char lowercase hex digest.
|
|
110
|
+
*/
|
|
111
|
+
export function contentFingerprint({ category, path, title } = {}) {
|
|
112
|
+
const norm = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
|
|
113
|
+
const canonical = `${norm(category)}|${norm(path)}|${norm(title)}`;
|
|
114
|
+
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
115
|
+
}
|
|
27
116
|
|
|
28
117
|
/**
|
|
29
|
-
* Spawn a child process and resolve to
|
|
30
|
-
* Never throws — spawn-time
|
|
118
|
+
* Spawn a child process and resolve to
|
|
119
|
+
* `{ code, stdout, stderr, spawnError, timedOut }`. Never throws — spawn-time
|
|
120
|
+
* errors are captured as `spawnError` and an overrun is captured as
|
|
121
|
+
* `timedOut: true` with a matching `spawnError`.
|
|
31
122
|
*
|
|
32
123
|
* This is the single spawn helper for the feedback-loop modules. Both
|
|
33
124
|
* graduators and `prior-feedback-fetcher.js` route their child-process
|
|
@@ -36,14 +127,26 @@ import { classifyPathSource as defaultClassifier } from '../observability/source
|
|
|
36
127
|
* options (e.g. omitting `cwd`) pass `undefined` and the option is
|
|
37
128
|
* dropped by the child_process layer.
|
|
38
129
|
*
|
|
130
|
+
* A caller-overridable `timeoutMs` (default {@link DEFAULT_RUN_CHILD_TIMEOUT_MS})
|
|
131
|
+
* bounds the wait: when it elapses the child is SIGKILL'd and the promise
|
|
132
|
+
* resolves with `{ code: null, timedOut: true, spawnError }` rather than
|
|
133
|
+
* hanging. Pass `0`/`Infinity` to disable the watchdog.
|
|
134
|
+
*
|
|
39
135
|
* @param {object} opts
|
|
40
136
|
* @param {string} opts.cmd — binary to spawn (e.g. "git", "gh")
|
|
41
137
|
* @param {string[]} opts.args — positional + flag arguments
|
|
42
138
|
* @param {Function} [opts.spawnImpl] — test seam; defaults to node:child_process spawn
|
|
43
139
|
* @param {string} [opts.cwd] — working directory for the child
|
|
44
|
-
* @
|
|
140
|
+
* @param {number} [opts.timeoutMs] — watchdog bound in ms
|
|
141
|
+
* @returns {Promise<{ code: number|null, stdout: string, stderr: string, spawnError: Error|null, timedOut: boolean }>}
|
|
45
142
|
*/
|
|
46
|
-
export function runChild({
|
|
143
|
+
export function runChild({
|
|
144
|
+
cmd,
|
|
145
|
+
args,
|
|
146
|
+
spawnImpl = defaultSpawn,
|
|
147
|
+
cwd,
|
|
148
|
+
timeoutMs = DEFAULT_RUN_CHILD_TIMEOUT_MS,
|
|
149
|
+
}) {
|
|
47
150
|
return new Promise((resolve) => {
|
|
48
151
|
let child;
|
|
49
152
|
try {
|
|
@@ -52,12 +155,55 @@ export function runChild({ cmd, args, spawnImpl = defaultSpawn, cwd }) {
|
|
|
52
155
|
cwd,
|
|
53
156
|
});
|
|
54
157
|
} catch (err) {
|
|
55
|
-
resolve({
|
|
158
|
+
resolve({
|
|
159
|
+
code: null,
|
|
160
|
+
stdout: '',
|
|
161
|
+
stderr: '',
|
|
162
|
+
spawnError: err,
|
|
163
|
+
timedOut: false,
|
|
164
|
+
});
|
|
56
165
|
return;
|
|
57
166
|
}
|
|
58
167
|
let stdout = '';
|
|
59
168
|
let stderr = '';
|
|
60
169
|
let spawnError = null;
|
|
170
|
+
let settled = false;
|
|
171
|
+
let timer = null;
|
|
172
|
+
const finish = (result) => {
|
|
173
|
+
if (settled) return;
|
|
174
|
+
settled = true;
|
|
175
|
+
if (timer) clearTimeout(timer);
|
|
176
|
+
resolve(result);
|
|
177
|
+
};
|
|
178
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
179
|
+
timer = setTimeout(() => {
|
|
180
|
+
try {
|
|
181
|
+
child.kill?.('SIGKILL');
|
|
182
|
+
} catch {
|
|
183
|
+
// Killing an already-dead / stub child is a no-op we ignore.
|
|
184
|
+
}
|
|
185
|
+
finish({
|
|
186
|
+
code: null,
|
|
187
|
+
stdout,
|
|
188
|
+
stderr,
|
|
189
|
+
spawnError: Object.assign(
|
|
190
|
+
new Error(
|
|
191
|
+
`child process '${cmd}' exceeded ${timeoutMs}ms and was killed`,
|
|
192
|
+
),
|
|
193
|
+
{ code: 'ETIMEDOUT' },
|
|
194
|
+
),
|
|
195
|
+
timedOut: true,
|
|
196
|
+
});
|
|
197
|
+
}, timeoutMs);
|
|
198
|
+
// Intentionally NOT unref'd: this is a watchdog timer that MUST keep
|
|
199
|
+
// the event loop alive until it fires (or the child settles). A real
|
|
200
|
+
// spawned child keeps the loop alive via its stdio handles, but a
|
|
201
|
+
// child whose handles close early — or a stub in tests — leaves the
|
|
202
|
+
// loop idle; an unref'd timer would then never fire, so the timeout
|
|
203
|
+
// silently would not bound a hung spawn (and the awaiting promise
|
|
204
|
+
// would hang forever). `finish()` always clearTimeout()s it, so the
|
|
205
|
+
// ref'd timer never outlives its purpose.
|
|
206
|
+
}
|
|
61
207
|
child.stdout?.on('data', (chunk) => {
|
|
62
208
|
stdout += chunk.toString();
|
|
63
209
|
});
|
|
@@ -68,7 +214,7 @@ export function runChild({ cmd, args, spawnImpl = defaultSpawn, cwd }) {
|
|
|
68
214
|
spawnError = err;
|
|
69
215
|
});
|
|
70
216
|
child.on('close', (code) => {
|
|
71
|
-
|
|
217
|
+
finish({ code, stdout, stderr, spawnError, timedOut: false });
|
|
72
218
|
});
|
|
73
219
|
});
|
|
74
220
|
}
|
|
@@ -79,7 +225,7 @@ export function runChild({ cmd, args, spawnImpl = defaultSpawn, cwd }) {
|
|
|
79
225
|
* toggle defaults to `true` and only an explicit `false` disables it.
|
|
80
226
|
*
|
|
81
227
|
* @param {string} toggleKey — key under `config.delivery.feedbackLoop`
|
|
82
|
-
* (e.g. "auditResultsAutoFile", "
|
|
228
|
+
* (e.g. "auditResultsAutoFile", "retroProposals")
|
|
83
229
|
* @returns {(config: object|undefined|null) => boolean}
|
|
84
230
|
*/
|
|
85
231
|
export function makeIsAutoFileEnabled(toggleKey) {
|
|
@@ -91,26 +237,40 @@ export function makeIsAutoFileEnabled(toggleKey) {
|
|
|
91
237
|
}
|
|
92
238
|
|
|
93
239
|
/**
|
|
94
|
-
* Probe whether the cited path exists in the merged tree at the given
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* `file-
|
|
240
|
+
* Probe whether the cited path exists in the merged tree at the given git
|
|
241
|
+
* ref, distinguishing a confirmed-missing file from a probe failure.
|
|
242
|
+
* Resolves `{ exists, probeError }`:
|
|
243
|
+
*
|
|
244
|
+
* - `git cat-file -e <ref>:<path>` exit 0 → `{ exists: true, probeError: false }`
|
|
245
|
+
* - clean non-zero exit (file genuinely absent) → `{ exists: false, probeError: false }`
|
|
246
|
+
* - spawn failure / timeout (cannot decide) → `{ exists: false, probeError: true }`
|
|
99
247
|
*
|
|
100
248
|
* @param {object} opts
|
|
101
249
|
* @param {string} opts.ref
|
|
102
250
|
* @param {string} opts.path
|
|
103
251
|
* @param {Function} [opts.spawnImpl]
|
|
104
252
|
* @param {string} [opts.cwd]
|
|
253
|
+
* @param {number} [opts.timeoutMs]
|
|
254
|
+
* @returns {Promise<{ exists: boolean, probeError: boolean }>}
|
|
105
255
|
*/
|
|
106
|
-
export async function
|
|
256
|
+
export async function probePathStatus({
|
|
257
|
+
ref,
|
|
258
|
+
path,
|
|
259
|
+
spawnImpl,
|
|
260
|
+
cwd,
|
|
261
|
+
timeoutMs,
|
|
262
|
+
}) {
|
|
107
263
|
const res = await runChild({
|
|
108
264
|
cmd: 'git',
|
|
109
265
|
args: ['cat-file', '-e', `${ref}:${path}`],
|
|
110
266
|
spawnImpl,
|
|
111
267
|
cwd,
|
|
268
|
+
timeoutMs,
|
|
112
269
|
});
|
|
113
|
-
|
|
270
|
+
if (res.spawnError || res.timedOut) {
|
|
271
|
+
return { exists: false, probeError: true };
|
|
272
|
+
}
|
|
273
|
+
return { exists: res.code === 0, probeError: false };
|
|
114
274
|
}
|
|
115
275
|
|
|
116
276
|
/**
|
|
@@ -127,6 +287,7 @@ export async function probeMarkerExists({
|
|
|
127
287
|
ghPath,
|
|
128
288
|
spawnImpl,
|
|
129
289
|
cwd,
|
|
290
|
+
timeoutMs,
|
|
130
291
|
}) {
|
|
131
292
|
const args = [
|
|
132
293
|
'search',
|
|
@@ -139,7 +300,7 @@ export async function probeMarkerExists({
|
|
|
139
300
|
'--limit',
|
|
140
301
|
'1',
|
|
141
302
|
];
|
|
142
|
-
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd });
|
|
303
|
+
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd, timeoutMs });
|
|
143
304
|
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
144
305
|
return false;
|
|
145
306
|
}
|
|
@@ -166,6 +327,7 @@ export async function createFollowUpIssue({
|
|
|
166
327
|
ghPath,
|
|
167
328
|
spawnImpl,
|
|
168
329
|
cwd,
|
|
330
|
+
timeoutMs,
|
|
169
331
|
}) {
|
|
170
332
|
const args = [
|
|
171
333
|
'issue',
|
|
@@ -180,7 +342,7 @@ export async function createFollowUpIssue({
|
|
|
180
342
|
for (const label of labels) {
|
|
181
343
|
args.push('--label', label);
|
|
182
344
|
}
|
|
183
|
-
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd });
|
|
345
|
+
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd, timeoutMs });
|
|
184
346
|
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
185
347
|
return {
|
|
186
348
|
url: null,
|
|
@@ -193,51 +355,6 @@ export async function createFollowUpIssue({
|
|
|
193
355
|
return { url, error: null };
|
|
194
356
|
}
|
|
195
357
|
|
|
196
|
-
/**
|
|
197
|
-
* Parametrized graduator walk. Reads the Epic's structured comment via
|
|
198
|
-
* the injected provider, parses non-blocking findings with the injected
|
|
199
|
-
* `parseFindings`, then for each finding runs the shared route → path
|
|
200
|
-
* probe → idempotency probe → file sequence. Never throws — every
|
|
201
|
-
* failure path is captured in `errors[]`.
|
|
202
|
-
*
|
|
203
|
-
* The per-graduator variation lives entirely in the injected callbacks:
|
|
204
|
-
*
|
|
205
|
-
* - `parseFindings(body)` — turns the rendered comment into findings.
|
|
206
|
-
* Each finding MUST carry `{ severity, path, summary, index }` and
|
|
207
|
-
* MAY carry additional fields (e.g. `lens`) that the builder uses.
|
|
208
|
-
* - `buildIdempotencyMarker(epicId, index)` — the HTML-comment marker
|
|
209
|
-
* embedded in (and searched for in) follow-up bodies.
|
|
210
|
-
* - `buildFollowUp({ finding, source, epicId })` — returns
|
|
211
|
-
* `{ title, body, labels }` for the issue to file.
|
|
212
|
-
* - `buildCrossRepoLog({ finding, routedRepo })` — returns the
|
|
213
|
-
* human-readable would-be-command string logged on a cross-repo skip.
|
|
214
|
-
* - `decorateRecord(record, finding)` — copies finding-specific fields
|
|
215
|
-
* (e.g. `lens`) onto a `skipped`/`filed` record before it is pushed.
|
|
216
|
-
*
|
|
217
|
-
* @param {object} opts
|
|
218
|
-
* @param {number} opts.epicId
|
|
219
|
-
* @param {object} opts.provider — exposes `getTicketComments(ticketId)`
|
|
220
|
-
* @param {object} [opts.config]
|
|
221
|
-
* @param {{owner: string, repo: string}} opts.currentRepo
|
|
222
|
-
* @param {{owner: string, repo: string}} [opts.frameworkRepo]
|
|
223
|
-
* @param {string} [opts.gitRef='HEAD']
|
|
224
|
-
* @param {Function} [opts.classifier=classifyPathSource]
|
|
225
|
-
* @param {string} [opts.ghPath='gh']
|
|
226
|
-
* @param {Function} [opts.spawnImpl]
|
|
227
|
-
* @param {string} [opts.cwd]
|
|
228
|
-
* @param {{info?: Function, warn?: Function, debug?: Function}} [opts.logger]
|
|
229
|
-
* @param {object} opts.spec — the per-graduator behaviour bundle
|
|
230
|
-
* @param {string} opts.spec.fnName — name used in error-message prefixes
|
|
231
|
-
* @param {(config: object|undefined|null) => boolean} opts.spec.isAutoFileEnabled
|
|
232
|
-
* @param {string} opts.spec.commentMarker — structured-comment marker to match
|
|
233
|
-
* @param {string} opts.spec.noCommentReason — skip reason when absent
|
|
234
|
-
* @param {Function} opts.spec.parseFindings
|
|
235
|
-
* @param {(epicId: number, index: number) => string} opts.spec.buildIdempotencyMarker
|
|
236
|
-
* @param {Function} opts.spec.buildFollowUp
|
|
237
|
-
* @param {Function} opts.spec.buildCrossRepoLog
|
|
238
|
-
* @param {(record: object, finding: object) => object} [opts.spec.decorateRecord]
|
|
239
|
-
* @returns {Promise<{ filed: object[], skipped: object[], errors: string[] }>}
|
|
240
|
-
*/
|
|
241
358
|
/**
|
|
242
359
|
* Validate the `graduate` preconditions (toggle, epicId, provider shape,
|
|
243
360
|
* currentRepo shape). Returns `null` when all preconditions pass, or a
|
|
@@ -301,10 +418,55 @@ async function loadGraduateFindings({ epicId, provider, spec }) {
|
|
|
301
418
|
return { findings };
|
|
302
419
|
}
|
|
303
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Probe whether a finding was already filed, checking both the current
|
|
423
|
+
* content-hash marker AND the legacy `(epicId, parse-index)` marker so
|
|
424
|
+
* findings filed before the fingerprint cutover are not re-filed. Returns
|
|
425
|
+
* the content-hash marker (embedded in a freshly filed body) alongside the
|
|
426
|
+
* `alreadyFiled` decision.
|
|
427
|
+
*/
|
|
428
|
+
async function resolveAlreadyFiled({
|
|
429
|
+
finding,
|
|
430
|
+
epicId,
|
|
431
|
+
routedRepo,
|
|
432
|
+
ghPath,
|
|
433
|
+
spawnImpl,
|
|
434
|
+
cwd,
|
|
435
|
+
timeoutMs,
|
|
436
|
+
spec,
|
|
437
|
+
}) {
|
|
438
|
+
const contentMarker = spec.buildContentMarker(epicId, finding);
|
|
439
|
+
const probe = (marker) =>
|
|
440
|
+
probeMarkerExists({
|
|
441
|
+
marker,
|
|
442
|
+
owner: routedRepo.owner,
|
|
443
|
+
repo: routedRepo.repo,
|
|
444
|
+
ghPath,
|
|
445
|
+
spawnImpl,
|
|
446
|
+
cwd,
|
|
447
|
+
timeoutMs,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (await probe(contentMarker)) {
|
|
451
|
+
return { alreadyFiled: true, contentMarker };
|
|
452
|
+
}
|
|
453
|
+
// Legacy recognition — a pre-cutover follow-up carries the ordinal
|
|
454
|
+
// marker, not the content hash. Skip re-filing when it is present.
|
|
455
|
+
if (typeof spec.buildLegacyMarker === 'function') {
|
|
456
|
+
const legacyMarker = spec.buildLegacyMarker(epicId, finding.index);
|
|
457
|
+
if (legacyMarker && (await probe(legacyMarker))) {
|
|
458
|
+
return { alreadyFiled: true, contentMarker };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { alreadyFiled: false, contentMarker };
|
|
462
|
+
}
|
|
463
|
+
|
|
304
464
|
/**
|
|
305
465
|
* Route a single finding (path-exists probe → repo routing → idempotency
|
|
306
|
-
* probe → file) and fold the outcome into the running envelope. Story
|
|
307
|
-
*
|
|
466
|
+
* probe → cap → file) and fold the outcome into the running envelope. Story
|
|
467
|
+
* #4075 extracted this from `graduate`'s per-finding loop; Story #4415
|
|
468
|
+
* hardened it (path-less seam, probe-error distinction, legacy-marker
|
|
469
|
+
* recognition, filing cap, and cross-repo-deferred collection).
|
|
308
470
|
*/
|
|
309
471
|
async function processGraduateFinding({
|
|
310
472
|
finding,
|
|
@@ -318,6 +480,9 @@ async function processGraduateFinding({
|
|
|
318
480
|
ghPath,
|
|
319
481
|
spawnImpl,
|
|
320
482
|
cwd,
|
|
483
|
+
timeoutMs,
|
|
484
|
+
maxFilingsPerRun,
|
|
485
|
+
crossRepoDeferred,
|
|
321
486
|
logger,
|
|
322
487
|
spec,
|
|
323
488
|
}) {
|
|
@@ -334,13 +499,24 @@ async function processGraduateFinding({
|
|
|
334
499
|
),
|
|
335
500
|
);
|
|
336
501
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
502
|
+
// Path-less findings (the retro auto-filer seam) are not file-scoped, so
|
|
503
|
+
// the path-exists gate does not apply — probing an empty path would
|
|
504
|
+
// misclassify them `file-removed`. Only file-scoped findings are probed.
|
|
505
|
+
const hasPath =
|
|
506
|
+
typeof finding.path === 'string' && finding.path.trim().length > 0;
|
|
507
|
+
if (hasPath) {
|
|
508
|
+
const { exists, probeError } = await probePathStatus({
|
|
509
|
+
ref: gitRef,
|
|
510
|
+
path: finding.path,
|
|
511
|
+
spawnImpl,
|
|
512
|
+
cwd,
|
|
513
|
+
timeoutMs,
|
|
514
|
+
});
|
|
515
|
+
// A probe failure is not proof the file is gone — record it distinctly
|
|
516
|
+
// so a transient git/spawn fault does not masquerade as a removal.
|
|
517
|
+
if (probeError) return skip('probe-error');
|
|
518
|
+
if (!exists) return skip('file-removed');
|
|
519
|
+
}
|
|
344
520
|
|
|
345
521
|
const source = classifier(finding.path, null);
|
|
346
522
|
const routedRepo =
|
|
@@ -349,26 +525,34 @@ async function processGraduateFinding({
|
|
|
349
525
|
routedRepo.owner !== currentRepo.owner ||
|
|
350
526
|
routedRepo.repo !== currentRepo.repo;
|
|
351
527
|
if (isCrossRepo) {
|
|
352
|
-
|
|
528
|
+
const logLine = spec.buildCrossRepoLog({ finding, routedRepo, source });
|
|
529
|
+
logger?.info?.(logLine);
|
|
530
|
+
crossRepoDeferred.push({ finding, routedRepo, source, logLine });
|
|
353
531
|
return skip('cross-repo-deferred');
|
|
354
532
|
}
|
|
355
533
|
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
repo: routedRepo.repo,
|
|
534
|
+
const { alreadyFiled, contentMarker } = await resolveAlreadyFiled({
|
|
535
|
+
finding,
|
|
536
|
+
epicId,
|
|
537
|
+
routedRepo,
|
|
361
538
|
ghPath,
|
|
362
539
|
spawnImpl,
|
|
363
540
|
cwd,
|
|
541
|
+
timeoutMs,
|
|
542
|
+
spec,
|
|
364
543
|
});
|
|
365
544
|
if (alreadyFiled) return skip('already-filed');
|
|
366
545
|
|
|
546
|
+
// Per-run filing cap — count only actual filings (already-filed and
|
|
547
|
+
// skipped findings do not consume the budget). The excess is surfaced so
|
|
548
|
+
// a re-run picks it up next time.
|
|
549
|
+
if (envelope.filed.length >= maxFilingsPerRun) return skip('cap-reached');
|
|
550
|
+
|
|
367
551
|
const { title, body, labels } = spec.buildFollowUp({
|
|
368
552
|
finding,
|
|
369
553
|
source,
|
|
370
554
|
epicId,
|
|
371
|
-
idMarker,
|
|
555
|
+
idMarker: contentMarker,
|
|
372
556
|
});
|
|
373
557
|
const created = await createFollowUpIssue({
|
|
374
558
|
owner: routedRepo.owner,
|
|
@@ -379,6 +563,7 @@ async function processGraduateFinding({
|
|
|
379
563
|
ghPath,
|
|
380
564
|
spawnImpl,
|
|
381
565
|
cwd,
|
|
566
|
+
timeoutMs,
|
|
382
567
|
});
|
|
383
568
|
if (created.error) {
|
|
384
569
|
envelope.errors.push(
|
|
@@ -401,6 +586,105 @@ async function processGraduateFinding({
|
|
|
401
586
|
);
|
|
402
587
|
}
|
|
403
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Render the durable cross-repo-deferred comment body from the collected
|
|
591
|
+
* deferrals. Each row names the finding path/severity plus the would-be
|
|
592
|
+
* `gh issue create` command so an operator (or a later cross-repo pass)
|
|
593
|
+
* can act on it.
|
|
594
|
+
*/
|
|
595
|
+
function renderCrossRepoDeferredBody(deferred, spec) {
|
|
596
|
+
const header =
|
|
597
|
+
spec.crossRepoCommentHeader ??
|
|
598
|
+
'### Cross-repo-deferred findings\n\nThese findings route to a different repository and were **not** filed here. They are recorded for a cross-repo follow-up pass.';
|
|
599
|
+
const rows = deferred.map(({ finding, routedRepo, logLine }) => {
|
|
600
|
+
const path =
|
|
601
|
+
typeof finding.path === 'string' && finding.path.length > 0
|
|
602
|
+
? `\`${finding.path}\``
|
|
603
|
+
: '_(no path)_';
|
|
604
|
+
return [
|
|
605
|
+
`- ${path} (severity: ${finding.severity ?? 'n/a'}) → ${routedRepo.owner}/${routedRepo.repo}`,
|
|
606
|
+
` - ${logLine}`,
|
|
607
|
+
].join('\n');
|
|
608
|
+
});
|
|
609
|
+
return [header, '', ...rows].join('\n');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Durably persist the cross-repo-deferred findings as a structured comment
|
|
614
|
+
* on the Epic (upserted — one comment per graduator, refreshed in place).
|
|
615
|
+
* Best-effort: a provider that cannot post comments is a no-op, and an
|
|
616
|
+
* upsert failure lands in `envelope.errors` rather than throwing.
|
|
617
|
+
*/
|
|
618
|
+
async function persistCrossRepoDeferred({
|
|
619
|
+
epicId,
|
|
620
|
+
provider,
|
|
621
|
+
crossRepoDeferred,
|
|
622
|
+
spec,
|
|
623
|
+
envelope,
|
|
624
|
+
}) {
|
|
625
|
+
if (typeof provider?.postComment !== 'function') return;
|
|
626
|
+
try {
|
|
627
|
+
const body = renderCrossRepoDeferredBody(crossRepoDeferred, spec);
|
|
628
|
+
await upsertStructuredComment(
|
|
629
|
+
provider,
|
|
630
|
+
epicId,
|
|
631
|
+
CROSS_REPO_DEFERRED_COMMENT_TYPE,
|
|
632
|
+
body,
|
|
633
|
+
spec.crossRepoCommentAttrs ?? null,
|
|
634
|
+
);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
envelope.errors.push(
|
|
637
|
+
`cross-repo-deferred comment upsert failed: ${err?.message ?? err}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Parametrized graduator walk. Parses non-blocking findings (from the
|
|
644
|
+
* Epic's structured comment, or a pre-parsed `findings` array), then for
|
|
645
|
+
* each finding runs the shared route → path probe → idempotency probe →
|
|
646
|
+
* cap → file sequence. Never throws — every failure path is captured in
|
|
647
|
+
* `errors[]`.
|
|
648
|
+
*
|
|
649
|
+
* The per-graduator variation lives entirely in the injected callbacks:
|
|
650
|
+
*
|
|
651
|
+
* - `parseFindings(body)` — turns the rendered comment into findings.
|
|
652
|
+
* Each finding MUST carry `{ severity, path, summary, index }` and
|
|
653
|
+
* MAY carry additional fields (e.g. `lens`) that the builder uses.
|
|
654
|
+
* - `buildContentMarker(epicId, finding)` — the content-hash HTML-comment
|
|
655
|
+
* marker embedded in (and searched for in) follow-up bodies.
|
|
656
|
+
* - `buildLegacyMarker(epicId, index)` — the pre-cutover ordinal marker,
|
|
657
|
+
* probed for idempotency so legacy filings are not duplicated.
|
|
658
|
+
* - `buildFollowUp({ finding, source, epicId, idMarker })` — returns
|
|
659
|
+
* `{ title, body, labels }` for the issue to file.
|
|
660
|
+
* - `buildCrossRepoLog({ finding, routedRepo, source })` — returns the
|
|
661
|
+
* human-readable would-be-command string for a cross-repo skip.
|
|
662
|
+
* - `decorateRecord(record, finding)` — copies finding-specific fields
|
|
663
|
+
* (e.g. `lens`) onto a `skipped`/`filed` record before it is pushed.
|
|
664
|
+
* - `crossRepoCommentAttrs` — discriminator attrs for the durable
|
|
665
|
+
* cross-repo-deferred comment (so the two graduators do not clobber
|
|
666
|
+
* each other's comment).
|
|
667
|
+
*
|
|
668
|
+
* @param {object} opts
|
|
669
|
+
* @param {number} opts.epicId
|
|
670
|
+
* @param {object} opts.provider — exposes `getTicketComments(ticketId)`
|
|
671
|
+
* @param {object} [opts.config]
|
|
672
|
+
* @param {{owner: string, repo: string}} opts.currentRepo
|
|
673
|
+
* @param {{owner: string, repo: string}} [opts.frameworkRepo]
|
|
674
|
+
* @param {string} [opts.gitRef='HEAD']
|
|
675
|
+
* @param {Function} [opts.classifier=classifyPathSource]
|
|
676
|
+
* @param {string} [opts.ghPath='gh']
|
|
677
|
+
* @param {Function} [opts.spawnImpl]
|
|
678
|
+
* @param {string} [opts.cwd]
|
|
679
|
+
* @param {number} [opts.timeoutMs] — per-spawn watchdog bound
|
|
680
|
+
* @param {number} [opts.maxFilingsPerRun] — per-run filing cap
|
|
681
|
+
* @param {Array<object>} [opts.findings] — pre-parsed findings; when
|
|
682
|
+
* provided, the structured-comment read/parse is bypassed (the retro
|
|
683
|
+
* auto-filer seam).
|
|
684
|
+
* @param {{info?: Function, warn?: Function, debug?: Function}} [opts.logger]
|
|
685
|
+
* @param {object} opts.spec — the per-graduator behaviour bundle
|
|
686
|
+
* @returns {Promise<{ filed: object[], skipped: object[], errors: string[] }>}
|
|
687
|
+
*/
|
|
404
688
|
export async function graduate({
|
|
405
689
|
epicId,
|
|
406
690
|
provider,
|
|
@@ -412,6 +696,9 @@ export async function graduate({
|
|
|
412
696
|
ghPath = 'gh',
|
|
413
697
|
spawnImpl,
|
|
414
698
|
cwd,
|
|
699
|
+
timeoutMs = DEFAULT_RUN_CHILD_TIMEOUT_MS,
|
|
700
|
+
maxFilingsPerRun = DEFAULT_MAX_FILINGS_PER_RUN,
|
|
701
|
+
findings: preParsedFindings,
|
|
415
702
|
logger,
|
|
416
703
|
spec,
|
|
417
704
|
}) {
|
|
@@ -430,10 +717,19 @@ export async function graduate({
|
|
|
430
717
|
});
|
|
431
718
|
if (precondition) return { ...envelope, ...precondition };
|
|
432
719
|
|
|
433
|
-
|
|
434
|
-
if (
|
|
720
|
+
let findings;
|
|
721
|
+
if (Array.isArray(preParsedFindings)) {
|
|
722
|
+
// Pre-parsed seam (retro auto-filer): bypass the structured-comment
|
|
723
|
+
// read + parse entirely and file the supplied findings directly.
|
|
724
|
+
findings = preParsedFindings;
|
|
725
|
+
} else {
|
|
726
|
+
const loaded = await loadGraduateFindings({ epicId, provider, spec });
|
|
727
|
+
if (!loaded.findings) return { ...envelope, ...loaded };
|
|
728
|
+
findings = loaded.findings;
|
|
729
|
+
}
|
|
435
730
|
|
|
436
|
-
|
|
731
|
+
const crossRepoDeferred = [];
|
|
732
|
+
for (const finding of findings) {
|
|
437
733
|
await processGraduateFinding({
|
|
438
734
|
finding,
|
|
439
735
|
envelope,
|
|
@@ -446,10 +742,23 @@ export async function graduate({
|
|
|
446
742
|
ghPath,
|
|
447
743
|
spawnImpl,
|
|
448
744
|
cwd,
|
|
745
|
+
timeoutMs,
|
|
746
|
+
maxFilingsPerRun,
|
|
747
|
+
crossRepoDeferred,
|
|
449
748
|
logger,
|
|
450
749
|
spec,
|
|
451
750
|
});
|
|
452
751
|
}
|
|
453
752
|
|
|
753
|
+
if (crossRepoDeferred.length > 0) {
|
|
754
|
+
await persistCrossRepoDeferred({
|
|
755
|
+
epicId,
|
|
756
|
+
provider,
|
|
757
|
+
crossRepoDeferred,
|
|
758
|
+
spec,
|
|
759
|
+
envelope,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
454
763
|
return envelope;
|
|
455
764
|
}
|