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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* planning-corpus.js — corpus-aware context for the standalone-Story
|
|
3
|
+
* planning path (Story #4432).
|
|
4
|
+
*
|
|
5
|
+
* `/plan --idea` previously drafted a standalone Story from a blank
|
|
6
|
+
* slate: the seed, the body template, and a title-only duplicate scan.
|
|
7
|
+
* For a change request that is really a small delta against an
|
|
8
|
+
* already-delivered surface, that blank slate throws away context the
|
|
9
|
+
* project already has — the docs digest and the relevant Tech Spec
|
|
10
|
+
* sections of existing Epics that cover the touched area.
|
|
11
|
+
*
|
|
12
|
+
* This module assembles that inherited context (`corpusContext`) for
|
|
13
|
+
* `story-plan.js`'s `--emit-context` envelope:
|
|
14
|
+
*
|
|
15
|
+
* 1. `docsDigest` — the same per-project docs digest
|
|
16
|
+
* `orchestration/docs-digest.js` builds for `/deliver` Story
|
|
17
|
+
* children, reused here so the standalone path gets the same
|
|
18
|
+
* compact outline instead of re-reading the whole docs set.
|
|
19
|
+
* `null` when `project.docsContextFiles` is not configured.
|
|
20
|
+
* 2. `relevantSections` — a ranked list of existing Epic Tech Spec
|
|
21
|
+
* (or lede, when no Tech Spec region exists) excerpts that overlap
|
|
22
|
+
* with the seed, so the draft can build on prior art instead of
|
|
23
|
+
* re-deriving it.
|
|
24
|
+
*
|
|
25
|
+
* The Epic list surface (`provider.getEpics`) maps every issue through
|
|
26
|
+
* `issueToEpicListItem`, which deliberately omits `body` (a list-scale
|
|
27
|
+
* payload trim). Body content therefore requires an **explicit**,
|
|
28
|
+
* bounded per-candidate fetch via `provider.getEpic(id)` — never a
|
|
29
|
+
* silent assumption that the list response carries prose to score
|
|
30
|
+
* against.
|
|
31
|
+
*
|
|
32
|
+
* Relevance scoring reuses the same `tokenize` / `overlapScore` Jaccard
|
|
33
|
+
* primitives `duplicate-search.js` exports for Epic-dedupe and
|
|
34
|
+
* `story-plan.js` reuses for Story-dedupe — one matcher, three
|
|
35
|
+
* consumers, no forked scoring logic.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { overlapScore, tokenize } from './duplicate-search.js';
|
|
39
|
+
import { extractEpicSection, hasEpicSection } from './epic-body-sections.js';
|
|
40
|
+
import { Logger } from './Logger.js';
|
|
41
|
+
import { buildDocsDigest } from './orchestration/docs-digest.js';
|
|
42
|
+
|
|
43
|
+
/** Top-K Epics kept after the cheap title-only ranking pass. */
|
|
44
|
+
const DEFAULT_CORPUS_MAX_CANDIDATES = 5;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bound on the explicit per-candidate body fetch. Keeps corpus-context
|
|
48
|
+
* assembly at a fixed, small number of GitHub reads regardless of how
|
|
49
|
+
* many open Epics the repo carries.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_CORPUS_BODY_FETCH_TOP_K = 3;
|
|
52
|
+
|
|
53
|
+
/** Minimum Jaccard overlap for a section excerpt to be worth surfacing. */
|
|
54
|
+
const DEFAULT_CORPUS_MIN_SCORE = 0.1;
|
|
55
|
+
|
|
56
|
+
/** Max relevant-section excerpts returned in the envelope. */
|
|
57
|
+
const DEFAULT_CORPUS_MAX_SECTIONS = 5;
|
|
58
|
+
|
|
59
|
+
/** Excerpt length cap (chars) so one oversized Tech Spec doesn't blow the envelope. */
|
|
60
|
+
const EXCERPT_MAX_CHARS = 600;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Page-scan cap passed to `provider.getEpics({ state: 'open', pageCap })`.
|
|
64
|
+
* The corpus lookup only ever ranks the list down to a top-5 shortlist
|
|
65
|
+
* (`DEFAULT_CORPUS_MAX_CANDIDATES`), so there is no need to inherit
|
|
66
|
+
* `paginateRest`'s full default ceiling (50 pages / 5000 items) to build
|
|
67
|
+
* it — a bounded scan keeps this call a fixed, small number of GitHub
|
|
68
|
+
* reads regardless of how many open Epics the repo carries.
|
|
69
|
+
*/
|
|
70
|
+
const CORPUS_EPICS_PAGE_CAP = 5;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Rank open Epics by title-overlap with the seed. This is the cheap
|
|
74
|
+
* first pass over the list surface (title only — `issueToEpicListItem`
|
|
75
|
+
* has no `body`), used solely to pick the bounded top-K candidates
|
|
76
|
+
* worth an explicit body fetch. It is not the final relevance signal;
|
|
77
|
+
* `extractRelevantSections` re-scores against actual section content.
|
|
78
|
+
*
|
|
79
|
+
* @param {{ seed: string, epics: Array<{ id:number, title:string }>, maxResults?: number }} opts
|
|
80
|
+
* @returns {Array<{ id:number, title:string, score:number }>}
|
|
81
|
+
*/
|
|
82
|
+
export function rankCandidateEpics({
|
|
83
|
+
seed,
|
|
84
|
+
epics,
|
|
85
|
+
maxResults = DEFAULT_CORPUS_MAX_CANDIDATES,
|
|
86
|
+
}) {
|
|
87
|
+
if (!seed || typeof seed !== 'string') {
|
|
88
|
+
throw new Error('rankCandidateEpics: seed must be a non-empty string');
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(epics)) {
|
|
91
|
+
throw new Error('rankCandidateEpics: epics must be an array');
|
|
92
|
+
}
|
|
93
|
+
const seedTokens = tokenize(seed);
|
|
94
|
+
if (seedTokens.size === 0) return [];
|
|
95
|
+
|
|
96
|
+
const ranked = [];
|
|
97
|
+
for (const epic of epics) {
|
|
98
|
+
if (!epic || typeof epic.title !== 'string') continue;
|
|
99
|
+
const score = overlapScore(seedTokens, tokenize(epic.title));
|
|
100
|
+
ranked.push({
|
|
101
|
+
id: epic.id,
|
|
102
|
+
title: epic.title,
|
|
103
|
+
score: Number(score.toFixed(4)),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
107
|
+
return ranked.slice(0, maxResults);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetch full bodies for the top-K ranked candidates via the single-issue
|
|
112
|
+
* read (`provider.getEpic`), which — unlike the `getEpics` list mapper —
|
|
113
|
+
* does carry `body`. This is the explicit, bounded fetch the corpus
|
|
114
|
+
* lookup performs instead of assuming the list surface already has
|
|
115
|
+
* prose to score: a candidate never contributes a relevant section
|
|
116
|
+
* without this round-trip resolving its body.
|
|
117
|
+
*
|
|
118
|
+
* A single candidate's fetch failing (deleted issue, transient error) is
|
|
119
|
+
* non-fatal — it is dropped from the result rather than aborting corpus
|
|
120
|
+
* assembly for every other candidate. Failures are logged via
|
|
121
|
+
* `Logger.debug` (stderr) so they are visible under
|
|
122
|
+
* `AGENT_LOG_LEVEL=verbose` triage without violating the friction-
|
|
123
|
+
* telemetry posture in `.agents/instructions.md` §1.H of never silently
|
|
124
|
+
* swallowing an error.
|
|
125
|
+
*
|
|
126
|
+
* The bounded candidate slice is fetched concurrently
|
|
127
|
+
* (`Promise.allSettled`) rather than sequentially — `topK` is a fixed
|
|
128
|
+
* small ceiling (default 3), so this is a bounded fan-out, not an
|
|
129
|
+
* unbounded one, and it removes the serial network-latency stacking a
|
|
130
|
+
* plain `for`-await loop would otherwise incur.
|
|
131
|
+
*
|
|
132
|
+
* @param {{ provider: object, candidates: Array<{ id:number, title:string }>, topK?: number }} opts
|
|
133
|
+
* @returns {Promise<Array<{ id:number, title:string, body:string }>>}
|
|
134
|
+
*/
|
|
135
|
+
export async function fetchCandidateBodies({
|
|
136
|
+
provider,
|
|
137
|
+
candidates,
|
|
138
|
+
topK = DEFAULT_CORPUS_BODY_FETCH_TOP_K,
|
|
139
|
+
}) {
|
|
140
|
+
if (!provider || typeof provider.getEpic !== 'function') return [];
|
|
141
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return [];
|
|
142
|
+
|
|
143
|
+
const bounded = candidates.slice(0, topK);
|
|
144
|
+
const settled = await Promise.allSettled(
|
|
145
|
+
bounded.map(async (candidate) => {
|
|
146
|
+
const epic = await provider.getEpic(candidate.id);
|
|
147
|
+
return {
|
|
148
|
+
id: candidate.id,
|
|
149
|
+
title: candidate.title ?? epic?.title ?? '',
|
|
150
|
+
body: epic?.body ?? '',
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const results = [];
|
|
156
|
+
for (let i = 0; i < settled.length; i += 1) {
|
|
157
|
+
const outcome = settled[i];
|
|
158
|
+
if (outcome.status === 'fulfilled') {
|
|
159
|
+
results.push(outcome.value);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Best-effort: one candidate failing to resolve must not abort
|
|
163
|
+
// corpus-context assembly for the rest — but the failure is still
|
|
164
|
+
// surfaced for triage rather than silently swallowed.
|
|
165
|
+
Logger.debug(
|
|
166
|
+
`[planning-corpus] fetchCandidateBodies: candidate #${bounded[i].id} failed to resolve: ${outcome.reason?.message ?? outcome.reason}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract a scoreable excerpt from an Epic body: the managed Tech Spec
|
|
174
|
+
* region when present (the folded `## Delivery Slicing` section, #4324),
|
|
175
|
+
* otherwise the ideation lede — the prose before the first `##` heading
|
|
176
|
+
* — so a plain-body Epic still contributes something to score.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} body
|
|
179
|
+
* @returns {{ kind:'techSpec'|'lede', content:string }}
|
|
180
|
+
*/
|
|
181
|
+
function extractScoreableExcerpt(body) {
|
|
182
|
+
if (hasEpicSection(body, 'techSpec')) {
|
|
183
|
+
return { kind: 'techSpec', content: extractEpicSection(body, 'techSpec') };
|
|
184
|
+
}
|
|
185
|
+
const lede = (body ?? '').split(/^##\s+/m)[0].trim();
|
|
186
|
+
return { kind: 'lede', content: lede };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Rank existing-Epic body excerpts (Tech Spec section, or lede) by
|
|
191
|
+
* overlap with the seed. Reuses the same `tokenize` / `overlapScore`
|
|
192
|
+
* primitives as the title-ranking pass above and as
|
|
193
|
+
* `duplicate-search.js` — one matcher shared across every corpus /
|
|
194
|
+
* dedupe surface.
|
|
195
|
+
*
|
|
196
|
+
* @param {{ seed:string, epicBodies: Array<{ id:number, title:string, body:string }>, maxResults?: number, minScore?: number }} opts
|
|
197
|
+
* @returns {Array<{ epicId:number, epicTitle:string, section:'techSpec'|'lede', score:number, excerpt:string }>}
|
|
198
|
+
*/
|
|
199
|
+
export function extractRelevantSections({
|
|
200
|
+
seed,
|
|
201
|
+
epicBodies,
|
|
202
|
+
maxResults = DEFAULT_CORPUS_MAX_SECTIONS,
|
|
203
|
+
minScore = DEFAULT_CORPUS_MIN_SCORE,
|
|
204
|
+
}) {
|
|
205
|
+
if (!seed || typeof seed !== 'string') {
|
|
206
|
+
throw new Error('extractRelevantSections: seed must be a non-empty string');
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(epicBodies)) {
|
|
209
|
+
throw new Error('extractRelevantSections: epicBodies must be an array');
|
|
210
|
+
}
|
|
211
|
+
const seedTokens = tokenize(seed);
|
|
212
|
+
if (seedTokens.size === 0) return [];
|
|
213
|
+
|
|
214
|
+
const ranked = [];
|
|
215
|
+
for (const epic of epicBodies) {
|
|
216
|
+
if (!epic) continue;
|
|
217
|
+
const { kind, content } = extractScoreableExcerpt(epic.body ?? '');
|
|
218
|
+
if (!content) continue;
|
|
219
|
+
const score = overlapScore(seedTokens, tokenize(content));
|
|
220
|
+
if (score < minScore) continue;
|
|
221
|
+
ranked.push({
|
|
222
|
+
epicId: epic.id,
|
|
223
|
+
epicTitle: epic.title ?? null,
|
|
224
|
+
section: kind,
|
|
225
|
+
score: Number(score.toFixed(4)),
|
|
226
|
+
excerpt: content.slice(0, EXCERPT_MAX_CHARS),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
230
|
+
return ranked.slice(0, maxResults);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Assemble the `corpusContext` field of the story-plan context envelope.
|
|
235
|
+
* Pure orchestration over the three helpers above plus `buildDocsDigest`
|
|
236
|
+
* — no I/O beyond what `provider` and the docs-digest reader perform.
|
|
237
|
+
*
|
|
238
|
+
* `relevantSections` is `[]` (not an error) when the provider has no
|
|
239
|
+
* `getEpics` surface, the seed tokenizes to nothing, or no candidate
|
|
240
|
+
* clears `minScore` — the standalone-Story draft path degrades to
|
|
241
|
+
* exactly today's blank-slate behavior in that case.
|
|
242
|
+
*
|
|
243
|
+
* @param {{
|
|
244
|
+
* seed: string,
|
|
245
|
+
* provider?: { getEpics?: Function, getEpic?: Function },
|
|
246
|
+
* docsContextFiles?: string[],
|
|
247
|
+
* docsRoot?: string,
|
|
248
|
+
* maxCandidates?: number,
|
|
249
|
+
* bodyFetchTopK?: number,
|
|
250
|
+
* maxSections?: number,
|
|
251
|
+
* minScore?: number,
|
|
252
|
+
* }} opts
|
|
253
|
+
* @returns {Promise<{ docsDigest: string|null, relevantSections: Array<object> }>}
|
|
254
|
+
*/
|
|
255
|
+
export async function buildCorpusContext({
|
|
256
|
+
seed,
|
|
257
|
+
provider,
|
|
258
|
+
docsContextFiles,
|
|
259
|
+
docsRoot,
|
|
260
|
+
maxCandidates = DEFAULT_CORPUS_MAX_CANDIDATES,
|
|
261
|
+
bodyFetchTopK = DEFAULT_CORPUS_BODY_FETCH_TOP_K,
|
|
262
|
+
maxSections = DEFAULT_CORPUS_MAX_SECTIONS,
|
|
263
|
+
minScore = DEFAULT_CORPUS_MIN_SCORE,
|
|
264
|
+
}) {
|
|
265
|
+
const docsDigest = await buildDocsDigest({ docsContextFiles, docsRoot });
|
|
266
|
+
|
|
267
|
+
let relevantSections = [];
|
|
268
|
+
if (provider && typeof provider.getEpics === 'function') {
|
|
269
|
+
// A single candidate-listing call failing (rate limit, transient
|
|
270
|
+
// network error, provider outage) must not abort the whole
|
|
271
|
+
// `--emit-context` envelope build — degrade to an empty candidate
|
|
272
|
+
// list instead of letting the rejection propagate out of the
|
|
273
|
+
// caller's `Promise.all` and take down the rest of the envelope
|
|
274
|
+
// (body template, tech-stack summary, docs digest) with it.
|
|
275
|
+
let epics = [];
|
|
276
|
+
try {
|
|
277
|
+
epics = await provider.getEpics({
|
|
278
|
+
state: 'open',
|
|
279
|
+
pageCap: CORPUS_EPICS_PAGE_CAP,
|
|
280
|
+
});
|
|
281
|
+
} catch (err) {
|
|
282
|
+
Logger.debug(
|
|
283
|
+
`[planning-corpus] buildCorpusContext: provider.getEpics failed, degrading to an empty candidate list: ${err?.message ?? err}`,
|
|
284
|
+
);
|
|
285
|
+
epics = [];
|
|
286
|
+
}
|
|
287
|
+
const ranked = rankCandidateEpics({
|
|
288
|
+
seed,
|
|
289
|
+
epics: Array.isArray(epics) ? epics : [],
|
|
290
|
+
maxResults: maxCandidates,
|
|
291
|
+
});
|
|
292
|
+
const bodies = await fetchCandidateBodies({
|
|
293
|
+
provider,
|
|
294
|
+
candidates: ranked,
|
|
295
|
+
topK: bodyFetchTopK,
|
|
296
|
+
});
|
|
297
|
+
relevantSections = extractRelevantSections({
|
|
298
|
+
seed,
|
|
299
|
+
epicBodies: bodies,
|
|
300
|
+
maxResults: maxSections,
|
|
301
|
+
minScore,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { docsDigest, relevantSections };
|
|
306
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* detectors/common.js — shared helpers for the signals layer.
|
|
3
3
|
*
|
|
4
|
-
* Hoisted out of
|
|
4
|
+
* Hoisted out of the detector modules (retry, rework) plus
|
|
5
5
|
* `signals/read.js` and `signals/schema.js`, all of which shipped
|
|
6
6
|
* byte-equivalent copies of these predicates. See Story #2464.
|
|
7
7
|
*/
|
|
@@ -19,15 +19,15 @@ export function isPositiveInt(v) {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Pull the tool name from a trace record. The hook writes the tool name
|
|
22
|
-
* into `
|
|
23
|
-
*
|
|
22
|
+
* into `emitter.tool` (canonical provenance) and, defensively, into
|
|
23
|
+
* `details.tool` — we accept either.
|
|
24
24
|
*
|
|
25
25
|
* @param {object} rec
|
|
26
26
|
* @returns {string|null}
|
|
27
27
|
*/
|
|
28
28
|
export function extractTool(rec) {
|
|
29
|
-
if (typeof rec?.
|
|
30
|
-
return rec.
|
|
29
|
+
if (typeof rec?.emitter?.tool === 'string' && rec.emitter.tool.length > 0) {
|
|
30
|
+
return rec.emitter.tool;
|
|
31
31
|
}
|
|
32
32
|
if (typeof rec?.details?.tool === 'string' && rec.details.tool.length > 0) {
|
|
33
33
|
return rec.details.tool;
|
|
@@ -38,12 +38,12 @@ export function extractTool(rec) {
|
|
|
38
38
|
/**
|
|
39
39
|
* Validate and normalize the shared detector argument preamble.
|
|
40
40
|
*
|
|
41
|
-
* `detectRework
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* `detectRework` and `detectRetry` previously shipped a near-identical
|
|
42
|
+
* guard block: the `args` object-shape `TypeError`, the `nowFn`
|
|
43
|
+
* function-type `TypeError`, the positive-integer `RangeError`s for the id
|
|
44
|
+
* fields, the non-empty-string `tracesPath` check, and the
|
|
45
45
|
* non-negative-integer `threshold` check. Story #4077 hoists that preamble
|
|
46
|
-
* here so the
|
|
46
|
+
* here so the detectors share one error-message contract.
|
|
47
47
|
*
|
|
48
48
|
* Error wording stays per-detector-accurate by prefixing every message with
|
|
49
49
|
* `fnName` (e.g. `detectRework: …`). Error *types* are preserved exactly:
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Detectors barrel (Epic #1721 / Story #1771 / Task #1774).
|
|
3
3
|
*
|
|
4
4
|
* Single import surface for every signal detector. Detector Stories
|
|
5
|
-
* (rework in #1771, retry in #1768
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* (rework in #1771, retry in #1768) re-export from here so callers
|
|
6
|
+
* (`lib/orchestration/detectors-phase.js`) only ever import from one
|
|
7
|
+
* place. The Epic #1769 hotspot detector was retired in the Epic #4406
|
|
8
|
+
* signal-contract cutover (no live emitter, no consumer).
|
|
8
9
|
*
|
|
9
10
|
* @module lib/signals/detectors
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
export { detectHotspot, nearestRankP95 } from './hotspot.js';
|
|
13
13
|
export { detectRetry } from './retry.js';
|
|
14
14
|
export { detectRework } from './rework.js';
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
* ## Failure rule
|
|
28
28
|
*
|
|
29
29
|
* A trace record is treated as **failed** when its `details.exitCode`
|
|
30
|
-
* is a number and not `0`.
|
|
31
|
-
*
|
|
32
|
-
* records without an `exitCode` field
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* is a number and not `0`. As of Epic #4406 / Story #4413 the tool-trace
|
|
31
|
+
* hook captures `details.exitCode` for Bash `PostToolUse` events, so this
|
|
32
|
+
* detector fires on real deliveries; records without an `exitCode` field
|
|
33
|
+
* (non-Bash tools, or tools that report no exit code) are ignored, which
|
|
34
|
+
* matches the decision in the parent Epic body that retry only counts
|
|
35
|
+
* non-zero-exit commands.
|
|
35
36
|
*
|
|
36
37
|
* Successful runs after failures **do not** cancel the count — failure-
|
|
37
38
|
* count is monotonic per identity. This matches the Epic's intent: once
|
|
@@ -41,12 +42,11 @@
|
|
|
41
42
|
*
|
|
42
43
|
* ## Tool filter
|
|
43
44
|
*
|
|
44
|
-
* Only trace records whose `
|
|
45
|
+
* Only trace records whose `emitter.tool === 'Bash'` participate. Edit /
|
|
45
46
|
* Write / Read / Grep / Glob events are not retries — those belong to
|
|
46
|
-
* other detectors (rework for file-edit churn
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
49
|
-
* trace shapes.
|
|
47
|
+
* other detectors (rework for file-edit churn). The tool name is read
|
|
48
|
+
* from `emitter.tool` first and falls back to `details.tool` (see
|
|
49
|
+
* `common.extractTool`).
|
|
50
50
|
*
|
|
51
51
|
* ## Privacy contract
|
|
52
52
|
*
|
|
@@ -124,13 +124,14 @@ function resolveIdentity(rec) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* Decide whether a trace record represents a failed invocation.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* NOT a failure and is ignored
|
|
133
|
-
* non-zero-exit commands per the
|
|
127
|
+
* Decide whether a trace record represents a failed invocation. As of
|
|
128
|
+
* Epic #4406 / Story #4413 the tool-trace hook records `details.exitCode`
|
|
129
|
+
* for Bash `PostToolUse` events, so the field is present on real Bash
|
|
130
|
+
* traces (and still set directly by tests). A record counts as failed
|
|
131
|
+
* when `details.exitCode` is a number and not zero. Anything else
|
|
132
|
+
* (missing field, null, non-number, zero) is NOT a failure and is ignored
|
|
133
|
+
* entirely — the detector only counts non-zero-exit commands per the
|
|
134
|
+
* parent Epic.
|
|
134
135
|
*
|
|
135
136
|
* @param {object} rec
|
|
136
137
|
* @returns {boolean}
|
|
@@ -238,7 +239,7 @@ export async function detectRetry(args) {
|
|
|
238
239
|
return offenders.map(([commandHash, failureCount]) => ({
|
|
239
240
|
ts,
|
|
240
241
|
kind: 'retry',
|
|
241
|
-
|
|
242
|
+
emitter: { tool: 'retry-detector' },
|
|
242
243
|
epicId,
|
|
243
244
|
storyId,
|
|
244
245
|
taskId,
|
|
@@ -18,48 +18,50 @@
|
|
|
18
18
|
* the codebase as of Epic #1181 (audit-snapshot 2026-05-11):
|
|
19
19
|
*
|
|
20
20
|
* Signals-writer `appendSignal` call sites:
|
|
21
|
-
* - `friction` — quality-gate / runtime friction (
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - `dispatched` — (no live emitter; retained pending Story #3908 sweep)
|
|
21
|
+
* - `friction` — quality-gate / runtime friction (diagnose-friction.js,
|
|
22
|
+
* lib/gates/friction.js, auto-refresh-runner.js,
|
|
23
|
+
* worktree-reap.js, lifecycle-emit.js)
|
|
24
|
+
* - `acceptance-eval` — acceptance-eval.js per-criterion terminus signal.
|
|
26
25
|
* - `wave-start` / `wave-complete` — `lib/wave-runner/tick.js` (read by
|
|
27
26
|
* perf-aggregator's `waveParallelism` report;
|
|
28
27
|
* `wave-start` also anchors span-tree Story spans)
|
|
29
|
-
* - `wave-end` — span-tree pairing anchor (
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* - `wave-end` — span-tree pairing anchor (retained for span-tree)
|
|
29
|
+
* - `state-transition` — notification-derived window anchor read by the
|
|
30
|
+
* waveParallelism bucketer.
|
|
32
31
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* rollup and nothing read them back.
|
|
32
|
+
* The `dispatched` kind was deleted in the Epic #4406 signal-contract
|
|
33
|
+
* cutover — it had no live emitter and no consumer.
|
|
36
34
|
*
|
|
37
35
|
* Signals-writer `appendTrace` call sites (traces.ndjson sibling, but
|
|
38
36
|
* sharing the same envelope shape — `tool-trace-hook.js`):
|
|
39
37
|
* - `trace` — per-tool-call timing record
|
|
40
38
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Detector-emitted / aggregator-consumed kinds (`rework`, `retry` are
|
|
40
|
+
* emitted by `lib/signals/detectors/*`; `hotspot`, `churn`, `idle` remain
|
|
41
|
+
* pinned in the enum for the aggregator's kind-count rollup even though no
|
|
42
|
+
* live detector emits them):
|
|
45
43
|
* - `hotspot`, `rework`, `churn`, `idle`, `retry`
|
|
46
44
|
*
|
|
47
|
-
* ## Common envelope
|
|
45
|
+
* ## Common envelope (canonical — Epic #4406 / Story #4413)
|
|
48
46
|
*
|
|
49
47
|
* Every signal MUST carry at minimum:
|
|
50
|
-
* - `ts` — ISO-8601 timestamp string
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `ts:` lands in a follow-on Story).
|
|
54
|
-
* - `epic` — integer Epic ID (writers historically used `epicId:`;
|
|
55
|
-
* same backward-compat note).
|
|
48
|
+
* - `ts` — ISO-8601 timestamp string. The single canonical
|
|
49
|
+
* timestamp key; the legacy `timestamp:` alias was deleted
|
|
50
|
+
* from every writer and this guard in the same PR.
|
|
56
51
|
* - `kind` — one of `EVENT_KINDS`.
|
|
57
52
|
*
|
|
53
|
+
* Scoped signals additionally carry:
|
|
54
|
+
* - `epicId` — integer Epic ID (or `null` for standalone-Story
|
|
55
|
+
* friction). The single canonical epic-id key; the legacy
|
|
56
|
+
* `epic:` alias was deleted.
|
|
57
|
+
*
|
|
58
58
|
* Optional but commonly carried:
|
|
59
|
-
* - `
|
|
60
|
-
* - `
|
|
61
|
-
* - `
|
|
62
|
-
* - `
|
|
59
|
+
* - `storyId` — integer Story ID
|
|
60
|
+
* - `taskId` — integer Task ID (nullable)
|
|
61
|
+
* - `emitter` — `{ tool: string, command?: string }` provenance
|
|
62
|
+
* - `source` — `"framework" | "consumer"` classifier tag
|
|
63
|
+
* - `category` — top-level friction category string
|
|
64
|
+
* - `details` — kind-specific payload (always an object)
|
|
63
65
|
*
|
|
64
66
|
* @module lib/signals/schema
|
|
65
67
|
*/
|
|
@@ -77,7 +79,6 @@ import { isPositiveInt } from './detectors/common.js';
|
|
|
77
79
|
export const EVENT_KINDS = Object.freeze({
|
|
78
80
|
FRICTION: 'friction',
|
|
79
81
|
TRACE: 'trace',
|
|
80
|
-
DISPATCHED: 'dispatched',
|
|
81
82
|
// Wave-window forensics signals: `wave-start` / `wave-end` anchor the
|
|
82
83
|
// span-tree's Story spans, and the perf-aggregator brackets each wave's
|
|
83
84
|
// wall-clock from `wave-start` → `wave-complete` (the `waveParallelism`
|
|
@@ -101,6 +102,13 @@ export const EVENT_KINDS = Object.freeze({
|
|
|
101
102
|
// the retro and /plan Phase 0 feedback fetch can see acceptance
|
|
102
103
|
// churn alongside friction/hotspot data.
|
|
103
104
|
ACCEPTANCE_EVAL: 'acceptance-eval',
|
|
105
|
+
// Forensic breadcrumb appended to the per-Epic stream by the notify
|
|
106
|
+
// dispatcher (lib/orchestration/lifecycle/listeners/notify-dispatcher.js)
|
|
107
|
+
// when a lifecycle event maps to a webhook notification — the resume
|
|
108
|
+
// suite reads it back to prove a dispatch survived a crash window without
|
|
109
|
+
// duplicating. Enumerated so the write-time validator (Story #4413) does
|
|
110
|
+
// not drop it; it is a deliberate write, not malformed data.
|
|
111
|
+
NOTIFICATION_EMITTED: 'notification.emitted',
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
/**
|
|
@@ -117,20 +125,17 @@ export const EVENT_KIND_VALUES = Object.freeze(
|
|
|
117
125
|
* spread string literals across the module graph.
|
|
118
126
|
*/
|
|
119
127
|
export const FIELDS = Object.freeze({
|
|
120
|
-
//
|
|
128
|
+
// Canonical envelope keys — there is exactly one key per concept. The
|
|
129
|
+
// legacy `timestamp` / `epic` / `story` / `task` aliases were deleted in
|
|
130
|
+
// the Epic #4406 signal-contract cutover; no reader tolerates them.
|
|
121
131
|
TS: 'ts',
|
|
122
|
-
EPIC: 'epic',
|
|
123
|
-
STORY: 'story',
|
|
124
|
-
TASK: 'task',
|
|
125
|
-
KIND: 'kind',
|
|
126
|
-
|
|
127
|
-
// Legacy envelope aliases still emitted by some writers
|
|
128
|
-
TIMESTAMP: 'timestamp',
|
|
129
132
|
EPIC_ID: 'epicId',
|
|
130
133
|
STORY_ID: 'storyId',
|
|
131
134
|
TASK_ID: 'taskId',
|
|
135
|
+
KIND: 'kind',
|
|
132
136
|
|
|
133
137
|
// Common payload fields
|
|
138
|
+
EMITTER: 'emitter',
|
|
134
139
|
SOURCE: 'source',
|
|
135
140
|
DETAILS: 'details',
|
|
136
141
|
CATEGORY: 'category',
|
|
@@ -150,8 +155,9 @@ function isTimestamp(v) {
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
/**
|
|
153
|
-
* Common envelope guard: every signal MUST carry `ts`
|
|
154
|
-
*
|
|
158
|
+
* Common envelope guard: every signal MUST carry `ts`, `epicId`, and a
|
|
159
|
+
* recognised `kind`. Canonical keys only — the legacy `timestamp` / `epic`
|
|
160
|
+
* aliases were deleted in the Epic #4406 cutover.
|
|
155
161
|
*
|
|
156
162
|
* Returns true when the envelope is well-formed. Used by `lib/signals/read`
|
|
157
163
|
* to discard malformed lines before yielding them to the consumer.
|
|
@@ -164,16 +170,24 @@ export function hasCommonEnvelope(evt) {
|
|
|
164
170
|
if (typeof evt.kind !== 'string' || !EVENT_KIND_VALUES.has(evt.kind)) {
|
|
165
171
|
return false;
|
|
166
172
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
if (!isTimestamp(evt.ts)) return false;
|
|
174
|
+
// The canonical `epicId` key MUST be present — a record carrying only the
|
|
175
|
+
// legacy `epic` alias is rejected (the cutover deleted that alias). Its
|
|
176
|
+
// value is nullable by contract: standalone-Story friction carries
|
|
177
|
+
// `epicId: null` (see signal-event.schema.json, where epicId is
|
|
178
|
+
// `["integer","null"]`). So accept an explicit null, reject a missing key
|
|
179
|
+
// or a present-but-non-positive-int value.
|
|
180
|
+
if (!Object.hasOwn(evt, 'epicId')) return false;
|
|
181
|
+
if (evt.epicId !== null && !isPositiveInt(evt.epicId)) return false;
|
|
171
182
|
return true;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
/**
|
|
175
186
|
* Generic per-kind guard. Returns true when the envelope is well-formed
|
|
176
|
-
* AND (when `kind` is supplied) the event's `kind` matches.
|
|
187
|
+
* AND (when `kind` is supplied) the event's `kind` matches. The full
|
|
188
|
+
* canonical-shape check lives in the AJV validator compiled from
|
|
189
|
+
* `signal-event.schema.json` (see `lib/observability/signal-validator.js`);
|
|
190
|
+
* this predicate is the cheap envelope gate the streaming reader uses.
|
|
177
191
|
*
|
|
178
192
|
* @param {unknown} evt
|
|
179
193
|
* @param {string} [kind] — optional kind to match (one of `EVENT_KINDS`).
|
|
@@ -184,42 +198,3 @@ export function isValidSignal(evt, kind) {
|
|
|
184
198
|
if (kind != null && evt.kind !== kind) return false;
|
|
185
199
|
return true;
|
|
186
200
|
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Per-kind shape guards. Each entry asserts the envelope plus any
|
|
190
|
-
* required per-kind fields. Unknown kinds fall back to the envelope
|
|
191
|
-
* check.
|
|
192
|
-
*
|
|
193
|
-
* The guards are intentionally lax — they reject records that are
|
|
194
|
-
* obviously malformed (missing `kind`, missing `ts`, missing `epic`),
|
|
195
|
-
* not records with extra fields or future schema extensions. The
|
|
196
|
-
* aggregator (perf-aggregator.js) carries its own per-kind narrowing.
|
|
197
|
-
*
|
|
198
|
-
* @type {Readonly<Record<string, (evt: unknown) => boolean>>}
|
|
199
|
-
*/
|
|
200
|
-
export const GUARDS = Object.freeze({
|
|
201
|
-
[EVENT_KINDS.FRICTION]: (evt) => {
|
|
202
|
-
if (!isValidSignal(evt, EVENT_KINDS.FRICTION)) return false;
|
|
203
|
-
// friction signals commonly carry a `category` field, but some
|
|
204
|
-
// writers (early in the migration) omit it. We accept both.
|
|
205
|
-
return true;
|
|
206
|
-
},
|
|
207
|
-
[EVENT_KINDS.TRACE]: (evt) => isValidSignal(evt, EVENT_KINDS.TRACE),
|
|
208
|
-
[EVENT_KINDS.DISPATCHED]: (evt) => isValidSignal(evt, EVENT_KINDS.DISPATCHED),
|
|
209
|
-
[EVENT_KINDS.WAVE_START]: (evt) => isValidSignal(evt, EVENT_KINDS.WAVE_START),
|
|
210
|
-
[EVENT_KINDS.WAVE_END]: (evt) => isValidSignal(evt, EVENT_KINDS.WAVE_END),
|
|
211
|
-
[EVENT_KINDS.WAVE_TICK]: (evt) => isValidSignal(evt, EVENT_KINDS.WAVE_TICK),
|
|
212
|
-
[EVENT_KINDS.WAVE_COMPLETE]: (evt) =>
|
|
213
|
-
isValidSignal(evt, EVENT_KINDS.WAVE_COMPLETE),
|
|
214
|
-
[EVENT_KINDS.EPIC_COMPLETE]: (evt) =>
|
|
215
|
-
isValidSignal(evt, EVENT_KINDS.EPIC_COMPLETE),
|
|
216
|
-
[EVENT_KINDS.STATE_TRANSITION]: (evt) =>
|
|
217
|
-
isValidSignal(evt, EVENT_KINDS.STATE_TRANSITION),
|
|
218
|
-
[EVENT_KINDS.HOTSPOT]: (evt) => isValidSignal(evt, EVENT_KINDS.HOTSPOT),
|
|
219
|
-
[EVENT_KINDS.REWORK]: (evt) => isValidSignal(evt, EVENT_KINDS.REWORK),
|
|
220
|
-
[EVENT_KINDS.CHURN]: (evt) => isValidSignal(evt, EVENT_KINDS.CHURN),
|
|
221
|
-
[EVENT_KINDS.IDLE]: (evt) => isValidSignal(evt, EVENT_KINDS.IDLE),
|
|
222
|
-
[EVENT_KINDS.RETRY]: (evt) => isValidSignal(evt, EVENT_KINDS.RETRY),
|
|
223
|
-
[EVENT_KINDS.ACCEPTANCE_EVAL]: (evt) =>
|
|
224
|
-
isValidSignal(evt, EVENT_KINDS.ACCEPTANCE_EVAL),
|
|
225
|
-
});
|