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
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { TYPE_LABELS } from '../../../label-constants.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
forEachEpicLine,
|
|
15
|
+
forEachLine,
|
|
16
|
+
} from '../../../observability/signals-writer.js';
|
|
14
17
|
import { concurrentMap } from '../../../util/concurrent-map.js';
|
|
18
|
+
import { read as readEpicRunState } from '../../epic-run-state-store.js';
|
|
15
19
|
import { composeRoutedProposals } from '../../retro-proposals.js';
|
|
16
20
|
import { parseFencedJsonComment } from '../../structured-comment-parser.js';
|
|
17
21
|
import { findStructuredComment } from '../../ticketing.js';
|
|
@@ -38,6 +42,15 @@ const SIGNALS_READ_CONCURRENCY = 8;
|
|
|
38
42
|
// zero-descendant walk is suspicious.
|
|
39
43
|
const EPIC_BODY_REFERENCE = /#\d+/;
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Fallback category stamped on a snapshot-derived blocked event that
|
|
47
|
+
* carries no `category` of its own. A non-empty category is what promotes
|
|
48
|
+
* the event to an actionable routed proposal in `composeRoutedProposals`,
|
|
49
|
+
* so a category-less `agent::blocked` record must still resolve to a
|
|
50
|
+
* concrete bucket rather than being silently dropped.
|
|
51
|
+
*/
|
|
52
|
+
const BLOCKED_EVENT_FALLBACK_CATEGORY = 'agent-blocked';
|
|
53
|
+
|
|
41
54
|
/**
|
|
42
55
|
* Pure: aggregate `frictionByCategory` payloads into a single integer.
|
|
43
56
|
*/
|
|
@@ -50,6 +63,83 @@ function sumFriction(byCategory) {
|
|
|
50
63
|
return total;
|
|
51
64
|
}
|
|
52
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Fold one parsed `signals.ndjson` record into the routed-signal
|
|
68
|
+
* accumulator. Shared by the per-Story and per-Epic scans so both streams
|
|
69
|
+
* contribute to the SAME unified counts that feed the routed proposals AND
|
|
70
|
+
* the compact/full retro decision. Reads the canonical envelope: top-level
|
|
71
|
+
* `category` and the string `source` classifier tag (Epic #4406). Non-object
|
|
72
|
+
* records and records without a `category` contribute nothing.
|
|
73
|
+
*
|
|
74
|
+
* @param {unknown} parsed
|
|
75
|
+
* @param {Array<{ category: string, source: 'framework'|'consumer' }>} routedSignals
|
|
76
|
+
*/
|
|
77
|
+
function accumulateSignalRecord(parsed, routedSignals) {
|
|
78
|
+
if (parsed === null || typeof parsed !== 'object') return;
|
|
79
|
+
const record = /** @type {Record<string, unknown>} */ (parsed);
|
|
80
|
+
const category = typeof record.category === 'string' ? record.category : null;
|
|
81
|
+
if (!category) return;
|
|
82
|
+
const source = record.source === 'framework' ? 'framework' : 'consumer';
|
|
83
|
+
routedSignals.push({ category, source });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read the `epic-run-state` snapshot and project every Story still recorded
|
|
88
|
+
* as `blocked` into an `unresolvedBlockedEvents` entry for
|
|
89
|
+
* `composeRoutedProposals`. Each event carries a **non-empty** category —
|
|
90
|
+
* the snapshot record's own `category` when present, otherwise the
|
|
91
|
+
* `agent-blocked` fallback — so it force-promotes an actionable proposal
|
|
92
|
+
* even when no friction signal shares its category. Best-effort: a read
|
|
93
|
+
* failure or shapeless snapshot degrades to an empty array (observability
|
|
94
|
+
* MUST NOT take down the retro path).
|
|
95
|
+
*
|
|
96
|
+
* @param {{
|
|
97
|
+
* epicId: number,
|
|
98
|
+
* provider: object,
|
|
99
|
+
* logger?: { warn?: Function },
|
|
100
|
+
* readFn: typeof readEpicRunState,
|
|
101
|
+
* }} args
|
|
102
|
+
* @returns {Promise<Array<{ ticketId?: number, category: string, source: 'framework'|'consumer' }>>}
|
|
103
|
+
*/
|
|
104
|
+
async function readUnresolvedBlockedEvents({
|
|
105
|
+
epicId,
|
|
106
|
+
provider,
|
|
107
|
+
logger,
|
|
108
|
+
readFn,
|
|
109
|
+
}) {
|
|
110
|
+
let snapshot;
|
|
111
|
+
try {
|
|
112
|
+
snapshot = await readFn({ provider, epicId });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger?.warn?.(
|
|
115
|
+
`[retro-runner] Failed to read epic-run-state for blocked events (continuing): ${
|
|
116
|
+
err?.message ?? err
|
|
117
|
+
}`,
|
|
118
|
+
);
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const storiesMap =
|
|
122
|
+
snapshot && typeof snapshot.stories === 'object' && snapshot.stories
|
|
123
|
+
? snapshot.stories
|
|
124
|
+
: {};
|
|
125
|
+
const events = [];
|
|
126
|
+
for (const [key, record] of Object.entries(storiesMap)) {
|
|
127
|
+
if (!record || typeof record !== 'object' || record.status !== 'blocked') {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const rawCategory =
|
|
131
|
+
typeof record.category === 'string' ? record.category.trim() : '';
|
|
132
|
+
const category =
|
|
133
|
+
rawCategory.length > 0 ? rawCategory : BLOCKED_EVENT_FALLBACK_CATEGORY;
|
|
134
|
+
const source = record.source === 'framework' ? 'framework' : 'consumer';
|
|
135
|
+
const ticketId = Number(key);
|
|
136
|
+
const event = { category, source };
|
|
137
|
+
if (Number.isInteger(ticketId) && ticketId > 0) event.ticketId = ticketId;
|
|
138
|
+
events.push(event);
|
|
139
|
+
}
|
|
140
|
+
return events;
|
|
141
|
+
}
|
|
142
|
+
|
|
53
143
|
/**
|
|
54
144
|
* Walk every descendant ticket of `epicId` once. Returns the flat list with
|
|
55
145
|
* each ticket's labels + body + state — the consumer derives its own
|
|
@@ -154,6 +244,8 @@ export async function gatherRetroSignals({
|
|
|
154
244
|
frameworkRepo,
|
|
155
245
|
consumerRepo,
|
|
156
246
|
forEachLineFn = forEachLine,
|
|
247
|
+
forEachEpicLineFn = forEachEpicLine,
|
|
248
|
+
epicRunStateReadFn = readEpicRunState,
|
|
157
249
|
composeRoutedProposalsFn = composeRoutedProposals,
|
|
158
250
|
}) {
|
|
159
251
|
const descendants = await collectDescendants(provider, epicId);
|
|
@@ -232,54 +324,35 @@ export async function gatherRetroSignals({
|
|
|
232
324
|
};
|
|
233
325
|
}
|
|
234
326
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
327
|
+
// Story #4417 — the compact/full retro decision, the automerge-verdict
|
|
328
|
+
// cleanSprint flag, and the routed proposals must all read from ONE
|
|
329
|
+
// signals scan. We fold the per-Story `signals.ndjson` streams AND the
|
|
330
|
+
// per-Epic `signals.ndjson` stream (the `appendEpicSignal` wave-lifecycle
|
|
331
|
+
// writers, e.g. lifecycle-emit) into a single `routedSignals`
|
|
332
|
+
// accumulation; `counts.friction` then derives from that same scan so a
|
|
333
|
+
// retro can never read "clean" from the comment substrate while
|
|
334
|
+
// actionable friction sits unrendered in the ndjson substrate.
|
|
335
|
+
//
|
|
336
|
+
// Story #2558 — the streams are source-tagged by the writer's
|
|
337
|
+
// `tagSignalSource`; `accumulateSignalRecord` reads the canonical
|
|
338
|
+
// envelope (top-level `category` + string `source`, Epic #4406). Read
|
|
339
|
+
// failures degrade silently — observability MUST NOT take down the retro.
|
|
248
340
|
//
|
|
249
|
-
// Story #3347 — the per-Story reads
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
// input order so we concatenate the per-Story results in `stories` order.
|
|
254
|
-
// That keeps `routedSignals` / `memorablePatterns` — and therefore the
|
|
255
|
-
// composed `routedProposals` — byte-for-byte identical to the prior
|
|
256
|
-
// serial behaviour, independent of which read settles first.
|
|
341
|
+
// Story #3347 — the per-Story reads fan out via `concurrentMap` with a
|
|
342
|
+
// bounded cap (`SIGNALS_READ_CONCURRENCY`); `concurrentMap` preserves
|
|
343
|
+
// input order so we concatenate the per-Story `routedSignals` in
|
|
344
|
+
// `stories` order, keeping the composed `routedProposals` deterministic.
|
|
257
345
|
const perStorySignals = await concurrentMap(
|
|
258
346
|
stories,
|
|
259
347
|
async (story) => {
|
|
260
348
|
const sid = Number(story.id ?? story.number);
|
|
261
349
|
if (!Number.isInteger(sid) || sid <= 0) {
|
|
262
|
-
return { routedSignals: []
|
|
350
|
+
return { routedSignals: [] };
|
|
263
351
|
}
|
|
264
352
|
const localRoutedSignals = [];
|
|
265
|
-
const localMemorablePatterns = [];
|
|
266
353
|
try {
|
|
267
354
|
await forEachLineFn(epicId, sid, (parsed) => {
|
|
268
|
-
|
|
269
|
-
const record = /** @type {Record<string, unknown>} */ (parsed);
|
|
270
|
-
const category =
|
|
271
|
-
typeof record.category === 'string' ? record.category : null;
|
|
272
|
-
const source =
|
|
273
|
-
record.source === 'framework' ? 'framework' : 'consumer';
|
|
274
|
-
if (category) {
|
|
275
|
-
localRoutedSignals.push({ category, source });
|
|
276
|
-
}
|
|
277
|
-
if (record.memorable === true && typeof record.insight === 'string') {
|
|
278
|
-
localMemorablePatterns.push({
|
|
279
|
-
category: category ?? 'general',
|
|
280
|
-
insight: record.insight,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
355
|
+
accumulateSignalRecord(parsed, localRoutedSignals);
|
|
283
356
|
});
|
|
284
357
|
} catch (err) {
|
|
285
358
|
logger?.warn?.(
|
|
@@ -288,24 +361,60 @@ export async function gatherRetroSignals({
|
|
|
288
361
|
}`,
|
|
289
362
|
);
|
|
290
363
|
}
|
|
291
|
-
return {
|
|
292
|
-
routedSignals: localRoutedSignals,
|
|
293
|
-
memorablePatterns: localMemorablePatterns,
|
|
294
|
-
};
|
|
364
|
+
return { routedSignals: localRoutedSignals };
|
|
295
365
|
},
|
|
296
366
|
{ concurrency: SIGNALS_READ_CONCURRENCY },
|
|
297
367
|
);
|
|
298
368
|
const routedSignals = [];
|
|
299
|
-
const memorablePatterns = [];
|
|
300
369
|
for (const perStory of perStorySignals) {
|
|
301
370
|
routedSignals.push(...perStory.routedSignals);
|
|
302
|
-
memorablePatterns.push(...perStory.memorablePatterns);
|
|
303
371
|
}
|
|
304
372
|
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
373
|
+
// Story #4417 — fold in the per-Epic `signals.ndjson` stream
|
|
374
|
+
// (`appendEpicSignal` wave-lifecycle writers, e.g. lifecycle-emit) so the
|
|
375
|
+
// unified scan is not blind to Epic-scoped friction that never lands on
|
|
376
|
+
// an individual Story stream.
|
|
377
|
+
try {
|
|
378
|
+
await forEachEpicLineFn(epicId, (parsed) => {
|
|
379
|
+
accumulateSignalRecord(parsed, routedSignals);
|
|
380
|
+
});
|
|
381
|
+
} catch (err) {
|
|
382
|
+
logger?.warn?.(
|
|
383
|
+
`[retro-runner] forEachEpicLine failed for epic #${epicId} (continuing): ${
|
|
384
|
+
err?.message ?? err
|
|
385
|
+
}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const counts = {
|
|
390
|
+
// `friction` derives from the unified ndjson scan (every categorized
|
|
391
|
+
// signal across the per-Story and per-Epic streams) PLUS the legacy
|
|
392
|
+
// story-perf-summary totals, so a non-empty ndjson substrate forces the
|
|
393
|
+
// full retro even when no `story-perf-summary` comment was posted.
|
|
394
|
+
friction: frictionFromSummaries + routedSignals.length,
|
|
395
|
+
parked: parkedFollowOns.parked.length,
|
|
396
|
+
recuts: parkedFollowOns.recuts.length,
|
|
397
|
+
hitl,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Story #4417 — project unresolved `agent::blocked` Stories from the
|
|
401
|
+
// epic-run-state snapshot into force-flag events for the routed-proposal
|
|
402
|
+
// composer. This replaces the formerly hardwired empty array: a blocked
|
|
403
|
+
// Story now force-promotes an actionable proposal even absent a matching
|
|
404
|
+
// friction category (its category falls back to `agent-blocked`).
|
|
405
|
+
const unresolvedBlockedEvents = await readUnresolvedBlockedEvents({
|
|
406
|
+
epicId,
|
|
407
|
+
provider,
|
|
408
|
+
logger,
|
|
409
|
+
readFn: epicRunStateReadFn,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Resolve repos. The framework repo falls back to the Mandrel mirror
|
|
413
|
+
// constant (a known, stable default). The consumer repo does NOT fall
|
|
414
|
+
// back to the framework repo: when the caller resolves no consumer repo
|
|
415
|
+
// from `config.github`, the consumer proposal pane is DISABLED loudly
|
|
416
|
+
// (Story #4417) rather than silently routing consumer-tagged friction at
|
|
417
|
+
// the framework mirror.
|
|
309
418
|
const resolvedFrameworkRepo =
|
|
310
419
|
typeof frameworkRepo === 'string' && frameworkRepo.length > 0
|
|
311
420
|
? frameworkRepo
|
|
@@ -313,15 +422,21 @@ export async function gatherRetroSignals({
|
|
|
313
422
|
const resolvedConsumerRepo =
|
|
314
423
|
typeof consumerRepo === 'string' && consumerRepo.length > 0
|
|
315
424
|
? consumerRepo
|
|
316
|
-
:
|
|
425
|
+
: '';
|
|
426
|
+
if (resolvedConsumerRepo.length === 0) {
|
|
427
|
+
logger?.warn?.(
|
|
428
|
+
'[retro-runner] No consumer repo resolved from config.github — the ' +
|
|
429
|
+
'consumer proposal pane is DISABLED for this retro. Set ' +
|
|
430
|
+
'github.owner / github.repo to route consumer-tagged friction.',
|
|
431
|
+
);
|
|
432
|
+
}
|
|
317
433
|
|
|
318
434
|
const routedProposals = composeRoutedProposalsFn({
|
|
319
435
|
epicId,
|
|
320
436
|
frameworkRepo: resolvedFrameworkRepo,
|
|
321
437
|
consumerRepo: resolvedConsumerRepo,
|
|
322
438
|
signals: routedSignals,
|
|
323
|
-
unresolvedBlockedEvents
|
|
324
|
-
memorablePatterns,
|
|
439
|
+
unresolvedBlockedEvents,
|
|
325
440
|
});
|
|
326
441
|
|
|
327
442
|
return {
|
|
@@ -14,6 +14,7 @@ import nodeFs from 'node:fs';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
|
|
16
16
|
import { epicRetroMirrorPath } from '../../../config/temp-paths.js';
|
|
17
|
+
import { fileRetroProposals as defaultFileRetroProposals } from '../../../feedback-loop/retro-proposals-graduator.js';
|
|
17
18
|
import { upsertStructuredComment } from '../../ticketing.js';
|
|
18
19
|
import { appendChecksSection, collectRetroFindings } from './checks.js';
|
|
19
20
|
import {
|
|
@@ -37,9 +38,15 @@ export async function composeAndPostRetro({
|
|
|
37
38
|
bus,
|
|
38
39
|
now,
|
|
39
40
|
manualInterventions,
|
|
41
|
+
frameworkRepo,
|
|
42
|
+
consumerRepo,
|
|
43
|
+
config = null,
|
|
40
44
|
gatherFn = defaultGatherRetroSignals,
|
|
41
45
|
composeFn = defaultComposeRetroBody,
|
|
42
46
|
upsertFn = upsertStructuredComment,
|
|
47
|
+
fileRetroProposalsFn = defaultFileRetroProposals,
|
|
48
|
+
ghPath,
|
|
49
|
+
spawnImpl,
|
|
43
50
|
runChecksFn,
|
|
44
51
|
assembleStateFn,
|
|
45
52
|
cwd,
|
|
@@ -48,7 +55,47 @@ export async function composeAndPostRetro({
|
|
|
48
55
|
onMirrorWritten,
|
|
49
56
|
perfThresholds = null,
|
|
50
57
|
}) {
|
|
51
|
-
const signals = await gatherFn({
|
|
58
|
+
const signals = await gatherFn({
|
|
59
|
+
epicId,
|
|
60
|
+
provider,
|
|
61
|
+
logger,
|
|
62
|
+
frameworkRepo,
|
|
63
|
+
consumerRepo,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Story #4418 — auto-file the retro's actionable routed proposals BEFORE
|
|
67
|
+
// the body composes, so the rendered retro sections list the real filed
|
|
68
|
+
// issue numbers instead of paste-ready `gh` command stanzas. Runs behind
|
|
69
|
+
// the `delivery.feedbackLoop.retroProposals` toggle (default ON); when
|
|
70
|
+
// OFF (or when filing is skipped / fails) the proposals pass through
|
|
71
|
+
// unenriched and the composer falls back to the command stanzas. Never
|
|
72
|
+
// throws — a filing failure degrades gracefully.
|
|
73
|
+
const { routedProposals: filedRoutedProposals, summary: filingSummary } =
|
|
74
|
+
await fileRetroProposalsFn({
|
|
75
|
+
epicId,
|
|
76
|
+
provider,
|
|
77
|
+
config,
|
|
78
|
+
frameworkRepo,
|
|
79
|
+
consumerRepo,
|
|
80
|
+
routedProposals: signals.routedProposals,
|
|
81
|
+
ghPath,
|
|
82
|
+
spawnImpl,
|
|
83
|
+
cwd,
|
|
84
|
+
logger,
|
|
85
|
+
});
|
|
86
|
+
// Surface (never throw on) per-proposal filing failures. Dropping
|
|
87
|
+
// `summary.errors` silently made a failed `gh issue create` — e.g. a
|
|
88
|
+
// `friction::<category>` label that doesn't exist in the target repo —
|
|
89
|
+
// invisible except as an unenriched command stanza in the retro body.
|
|
90
|
+
if (Array.isArray(filingSummary?.errors) && filingSummary.errors.length) {
|
|
91
|
+
for (const err of filingSummary.errors) {
|
|
92
|
+
logger?.warn?.(
|
|
93
|
+
`[retro] proposal auto-file error (degrading to command stanza): ${
|
|
94
|
+
typeof err === 'string' ? err : JSON.stringify(err)
|
|
95
|
+
}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
52
99
|
|
|
53
100
|
// Best-effort fetch of the Epic title for the heading.
|
|
54
101
|
let epicTitle;
|
|
@@ -69,7 +116,7 @@ export async function composeAndPostRetro({
|
|
|
69
116
|
storyPerfSummaries: signals.storyPerfSummaries,
|
|
70
117
|
epicPerfReport: signals.epicPerfReport,
|
|
71
118
|
parkedFollowOns: signals.parkedFollowOns,
|
|
72
|
-
routedProposals:
|
|
119
|
+
routedProposals: filedRoutedProposals,
|
|
73
120
|
timestamp,
|
|
74
121
|
forceFull,
|
|
75
122
|
perfThresholds,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/orchestration/retro-proposals.js — pure composer that turns
|
|
3
|
-
* aggregated source-tagged friction signals into
|
|
4
|
-
* sections (framework, consumer,
|
|
3
|
+
* aggregated source-tagged friction signals into three routed proposal
|
|
4
|
+
* sections (framework, consumer, discarded).
|
|
5
5
|
*
|
|
6
6
|
* Epic #2547 / Story #2558 / Tech Spec #2550. Consumes per-Story signals
|
|
7
|
-
* already source-tagged by `signals-writer.appendSignal`
|
|
8
|
-
*
|
|
9
|
-
* `<!-- retro-complete: ... -->` marker.
|
|
7
|
+
* already source-tagged by `signals-writer.appendSignal` and yields a
|
|
8
|
+
* three-way split that the retro composer renders above the
|
|
9
|
+
* `<!-- retro-complete: ... -->` marker. The former "memory updates"
|
|
10
|
+
* pane was deleted in the Epic #4406 signal-contract cutover (it had no
|
|
11
|
+
* producer — no writer ever emitted the record it rendered).
|
|
10
12
|
*
|
|
11
13
|
* Heuristic:
|
|
12
14
|
* - **Actionable** (renders as a pre-drafted `gh issue create` shell
|
|
@@ -14,14 +16,8 @@
|
|
|
14
16
|
* Epic, OR an `agent::blocked` event whose root cause was not
|
|
15
17
|
* resolved by Epic close (the caller supplies these as
|
|
16
18
|
* `unresolvedBlockedEvents`).
|
|
17
|
-
* - **Memorable** (renders as a plain bulleted instruction line under
|
|
18
|
-
* "update your memory with the following insights"): a pattern
|
|
19
|
-
* observed in retro signals supplied via `memorablePatterns`. We do
|
|
20
|
-
* **not** emit memory frontmatter — the section is a free-text
|
|
21
|
-
* instruction block.
|
|
22
19
|
* - **Discarded**: a friction category with exactly 1 occurrence and
|
|
23
|
-
* no follow-on signal (no companion `agent::blocked
|
|
24
|
-
* `memorablePatterns`).
|
|
20
|
+
* no follow-on signal (no companion `agent::blocked`).
|
|
25
21
|
*
|
|
26
22
|
* Routing:
|
|
27
23
|
* - Each actionable item is routed to `framework` or `consumer` based
|
|
@@ -45,17 +41,12 @@
|
|
|
45
41
|
* @property {string} [category]
|
|
46
42
|
* @property {string} [summary]
|
|
47
43
|
*
|
|
48
|
-
* @typedef {Object} MemorablePattern
|
|
49
|
-
* @property {string} category
|
|
50
|
-
* @property {string} insight The instruction line text (rendered as a bullet).
|
|
51
|
-
*
|
|
52
44
|
* @typedef {Object} RoutedProposalsInput
|
|
53
45
|
* @property {number} epicId
|
|
54
46
|
* @property {string} frameworkRepo `"<owner>/<repo>"`.
|
|
55
47
|
* @property {string} consumerRepo `"<owner>/<repo>"`.
|
|
56
48
|
* @property {FrictionSignal[]} [signals]
|
|
57
49
|
* @property {BlockedEvent[]} [unresolvedBlockedEvents]
|
|
58
|
-
* @property {MemorablePattern[]} [memorablePatterns]
|
|
59
50
|
*
|
|
60
51
|
* @typedef {Object} RoutedItem
|
|
61
52
|
* @property {string} category
|
|
@@ -65,10 +56,6 @@
|
|
|
65
56
|
* @property {string} body
|
|
66
57
|
* @property {string} command The pre-drafted `gh issue create` line.
|
|
67
58
|
*
|
|
68
|
-
* @typedef {Object} MemoryItem
|
|
69
|
-
* @property {string} category
|
|
70
|
-
* @property {string} insight
|
|
71
|
-
*
|
|
72
59
|
* @typedef {Object} DiscardedItem
|
|
73
60
|
* @property {string} category
|
|
74
61
|
* @property {number} occurrences
|
|
@@ -77,7 +64,6 @@
|
|
|
77
64
|
* @typedef {Object} RoutedProposals
|
|
78
65
|
* @property {RoutedItem[]} framework
|
|
79
66
|
* @property {RoutedItem[]} consumer
|
|
80
|
-
* @property {MemoryItem[]} memory
|
|
81
67
|
* @property {DiscardedItem[]} discarded
|
|
82
68
|
*/
|
|
83
69
|
|
|
@@ -88,7 +74,7 @@
|
|
|
88
74
|
* @returns {RoutedProposals}
|
|
89
75
|
*/
|
|
90
76
|
function emptyResult() {
|
|
91
|
-
return { framework: [], consumer: [],
|
|
77
|
+
return { framework: [], consumer: [], discarded: [] };
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
/**
|
|
@@ -250,7 +236,6 @@ function buildRoutedItem({
|
|
|
250
236
|
* consumerRepo: string,
|
|
251
237
|
* signals: FrictionSignal[],
|
|
252
238
|
* unresolvedBlockedEvents: BlockedEvent[],
|
|
253
|
-
* memorablePatterns: MemorablePattern[],
|
|
254
239
|
* } | null}
|
|
255
240
|
*/
|
|
256
241
|
function normaliseInput(input) {
|
|
@@ -269,9 +254,6 @@ function normaliseInput(input) {
|
|
|
269
254
|
unresolvedBlockedEvents: Array.isArray(record.unresolvedBlockedEvents)
|
|
270
255
|
? record.unresolvedBlockedEvents
|
|
271
256
|
: [],
|
|
272
|
-
memorablePatterns: Array.isArray(record.memorablePatterns)
|
|
273
|
-
? record.memorablePatterns
|
|
274
|
-
: [],
|
|
275
257
|
};
|
|
276
258
|
}
|
|
277
259
|
|
|
@@ -280,11 +262,9 @@ function normaliseInput(input) {
|
|
|
280
262
|
* signals.
|
|
281
263
|
*
|
|
282
264
|
* Pure — no I/O, no time-dependent state, no provider calls. Returns an
|
|
283
|
-
* object with
|
|
265
|
+
* object with three arrays:
|
|
284
266
|
* - `framework`: actionable items routed to the framework repo.
|
|
285
267
|
* - `consumer`: actionable items routed to the consumer repo.
|
|
286
|
-
* - `memory`: bulleted instruction lines for the operator's memory
|
|
287
|
-
* surface (NOT memory frontmatter).
|
|
288
268
|
* - `discarded`: single-occurrence friction with no follow-on signal.
|
|
289
269
|
*
|
|
290
270
|
* @param {RoutedProposalsInput} input
|
|
@@ -299,28 +279,10 @@ export function composeRoutedProposals(input) {
|
|
|
299
279
|
consumerRepo,
|
|
300
280
|
signals,
|
|
301
281
|
unresolvedBlockedEvents,
|
|
302
|
-
memorablePatterns,
|
|
303
282
|
} = normalised;
|
|
304
283
|
|
|
305
284
|
const byCategory = aggregateByCategory(signals);
|
|
306
285
|
|
|
307
|
-
// Memory: every supplied pattern with a non-empty insight, sorted by
|
|
308
|
-
// category. Memorable categories are *also* tracked so a 1-occurrence
|
|
309
|
-
// friction that's memorable is NOT discarded — it's already covered by
|
|
310
|
-
// the memory section.
|
|
311
|
-
const memorableCategories = new Set();
|
|
312
|
-
/** @type {MemoryItem[]} */
|
|
313
|
-
const memory = [];
|
|
314
|
-
for (const m of memorablePatterns) {
|
|
315
|
-
if (m === null || typeof m !== 'object') continue;
|
|
316
|
-
const category = asString(m.category);
|
|
317
|
-
const insight = asString(m.insight);
|
|
318
|
-
if (category.length === 0 || insight.length === 0) continue;
|
|
319
|
-
memorableCategories.add(category);
|
|
320
|
-
memory.push({ category, insight });
|
|
321
|
-
}
|
|
322
|
-
memory.sort((a, b) => a.category.localeCompare(b.category));
|
|
323
|
-
|
|
324
286
|
// Unresolved agent::blocked events always promote their category to
|
|
325
287
|
// actionable — even if the friction count is < 2. The event itself
|
|
326
288
|
// doesn't count as a friction occurrence; we treat it as a force-flag.
|
|
@@ -362,12 +324,7 @@ export function composeRoutedProposals(input) {
|
|
|
362
324
|
else consumer.push(item);
|
|
363
325
|
continue;
|
|
364
326
|
}
|
|
365
|
-
// total === 1 AND no force flag.
|
|
366
|
-
if (memorableCategories.has(category)) {
|
|
367
|
-
// Memorable single-occurrence frictions are covered by the memory
|
|
368
|
-
// section; do not also discard them.
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
327
|
+
// total === 1 AND no force flag → discarded.
|
|
371
328
|
discarded.push({ category, occurrences: total, source });
|
|
372
329
|
}
|
|
373
330
|
|
|
@@ -391,5 +348,5 @@ export function composeRoutedProposals(input) {
|
|
|
391
348
|
consumer.sort((a, b) => a.category.localeCompare(b.category));
|
|
392
349
|
discarded.sort((a, b) => a.category.localeCompare(b.category));
|
|
393
350
|
|
|
394
|
-
return { framework, consumer,
|
|
351
|
+
return { framework, consumer, discarded };
|
|
395
352
|
}
|
|
@@ -95,6 +95,9 @@ export { gatherRetroSignals } from './retro/phases/gather-signals.js';
|
|
|
95
95
|
* bus?: object|null,
|
|
96
96
|
* now?: () => number,
|
|
97
97
|
* manualInterventions?: number,
|
|
98
|
+
* frameworkRepo?: string,
|
|
99
|
+
* consumerRepo?: string,
|
|
100
|
+
* config?: object|null,
|
|
98
101
|
* gatherFn?: typeof gatherRetroSignals,
|
|
99
102
|
* composeFn?: typeof composeRetroBody,
|
|
100
103
|
* upsertFn?: typeof upsertStructuredComment,
|
|
@@ -122,6 +125,9 @@ export async function runRetro(opts = {}) {
|
|
|
122
125
|
bus,
|
|
123
126
|
now = Date.now,
|
|
124
127
|
manualInterventions = 0,
|
|
128
|
+
frameworkRepo,
|
|
129
|
+
consumerRepo,
|
|
130
|
+
config = null,
|
|
125
131
|
gatherFn = gatherRetroSignals,
|
|
126
132
|
composeFn = composeRetroBody,
|
|
127
133
|
upsertFn = upsertStructuredComment,
|
|
@@ -159,6 +165,9 @@ export async function runRetro(opts = {}) {
|
|
|
159
165
|
bus,
|
|
160
166
|
now,
|
|
161
167
|
manualInterventions,
|
|
168
|
+
frameworkRepo,
|
|
169
|
+
consumerRepo,
|
|
170
|
+
config,
|
|
162
171
|
gatherFn,
|
|
163
172
|
composeFn,
|
|
164
173
|
upsertFn,
|
|
@@ -94,6 +94,7 @@ export function buildStoryReviewCrossRefBody({
|
|
|
94
94
|
* prNumber: number|null,
|
|
95
95
|
* provider: object,
|
|
96
96
|
* runCodeReviewFn: Function,
|
|
97
|
+
* runLocalLensReviewFn?: Function,
|
|
97
98
|
* progress: (tag: string, msg: string) => void,
|
|
98
99
|
* }} args
|
|
99
100
|
* @returns {Promise<{
|
|
@@ -103,6 +104,7 @@ export function buildStoryReviewCrossRefBody({
|
|
|
103
104
|
* posted?: boolean,
|
|
104
105
|
* postedCommentId?: number|null,
|
|
105
106
|
* crossRefPosted?: boolean,
|
|
107
|
+
* localLensReview?: object,
|
|
106
108
|
* }>}
|
|
107
109
|
*/
|
|
108
110
|
export async function runStoryScopeReview({
|
|
@@ -114,6 +116,7 @@ export async function runStoryScopeReview({
|
|
|
114
116
|
prNumber,
|
|
115
117
|
provider,
|
|
116
118
|
runCodeReviewFn,
|
|
119
|
+
runLocalLensReviewFn,
|
|
117
120
|
progress,
|
|
118
121
|
}) {
|
|
119
122
|
if (prNumber == null) {
|
|
@@ -138,6 +141,10 @@ export async function runStoryScopeReview({
|
|
|
138
141
|
progress,
|
|
139
142
|
progressTag: 'REVIEW',
|
|
140
143
|
runCodeReviewFn,
|
|
144
|
+
// Forward the seam only when the caller injects it; otherwise
|
|
145
|
+
// `runStoryReviewCore` uses its default local-lens pass. `undefined`
|
|
146
|
+
// deep-merges to the default via the destructuring default there.
|
|
147
|
+
...(runLocalLensReviewFn ? { runLocalLensReviewFn } : {}),
|
|
141
148
|
});
|
|
142
149
|
|
|
143
150
|
const sev = result.severity ?? {
|
|
@@ -186,5 +193,6 @@ export async function runStoryScopeReview({
|
|
|
186
193
|
posted: result.posted,
|
|
187
194
|
postedCommentId: result.postedCommentId ?? null,
|
|
188
195
|
crossRefPosted,
|
|
196
|
+
localLensReview: result.localLensReview,
|
|
189
197
|
};
|
|
190
198
|
}
|