mandrel 1.68.0 → 1.70.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 +1 -1
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +2 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/schemas/agentrc.schema.json +1 -5
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
- package/.agents/scripts/agents-update-preflight.js +235 -0
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/epic-deliver-preflight.js +30 -13
- package/.agents/scripts/epic-deliver-prepare.js +40 -53
- package/.agents/scripts/epic-execute-record-wave.js +119 -133
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +13 -5
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/explain.js +0 -2
- package/.agents/scripts/lib/config/limits.js +19 -8
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-settings-schema.js +1 -2
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/maintainability-utils.js +32 -9
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -162
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
- package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
- package/.agents/scripts/lib/story-adjacency.js +14 -10
- package/.agents/scripts/lib/story-body/story-body.js +142 -65
- package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
- package/.agents/scripts/lib/wave-runner/tick.js +446 -216
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/lint-label-vocabulary.js +1 -1
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/stories-wave-tick.js +262 -161
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/agents-update.md +205 -28
- package/.agents/workflows/deliver.md +12 -9
- package/.agents/workflows/helpers/deliver-epic.md +126 -90
- package/.agents/workflows/helpers/deliver-stories.md +131 -85
- package/.agents/workflows/helpers/plan-epic.md +13 -10
- package/.agents/workflows/plan.md +1 -1
- package/README.md +20 -0
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- package/.agents/scripts/lib/wave-runner/wave-checkpoint.js +0 -91
|
@@ -67,10 +67,9 @@ export const PHASE_ORDER = [
|
|
|
67
67
|
];
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Emoji prefix per high-level state.
|
|
71
|
-
* (`composition.
|
|
72
|
-
*
|
|
73
|
-
* in both surfaces.
|
|
70
|
+
* Emoji prefix per high-level state. Consumed by the rolled-up Epic table
|
|
71
|
+
* (`composition.upsertEpicRunProgress`) so operators see a consistent icon
|
|
72
|
+
* per state across the progress surface.
|
|
74
73
|
*/
|
|
75
74
|
export const STATE_EMOJI = {
|
|
76
75
|
done: '✅',
|
|
@@ -38,10 +38,10 @@ export const EPIC_PROGRESS_EVENT = 'epic-progress';
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Fire a curated `epic-progress` webhook event. Event-driven only — called
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* per recorder beat and after blocker raise/clear transitions. Carries the
|
|
42
|
+
* rollup payload `{ pct, done, total, phase, openBlockers }`, which Slack
|
|
43
|
+
* consumers and downstream subscribers use to track epic progress without
|
|
44
|
+
* subscribing to per-story chatter.
|
|
45
45
|
*
|
|
46
46
|
* The dispatch passes `skipComment: true` — the operator-facing GitHub
|
|
47
47
|
* comment is owned by `ProgressReporter.fire()` and `upsertEpicRunProgress`,
|
|
@@ -50,13 +50,17 @@ export const EPIC_PROGRESS_EVENT = 'epic-progress';
|
|
|
50
50
|
* Failures are swallowed by design: the runner must keep moving even if
|
|
51
51
|
* the webhook URL is misconfigured or the network is flaky.
|
|
52
52
|
*
|
|
53
|
+
* Story #4155 — the Epic `/deliver` runtime cut over to the continuous
|
|
54
|
+
* ready-set scheduler, which has **no wave index**. The wave segment was
|
|
55
|
+
* dropped from the message and the rollup payload entirely (rather than
|
|
56
|
+
* rendered as `Wave undefined/undefined`); the sole live caller
|
|
57
|
+
* (`wave-record-notifications.js`) never supplied wave coordinates.
|
|
58
|
+
*
|
|
53
59
|
* @param {{
|
|
54
60
|
* notify: Function|null,
|
|
55
61
|
* epicId: number,
|
|
56
62
|
* done: number,
|
|
57
63
|
* total: number,
|
|
58
|
-
* currentWave: number,
|
|
59
|
-
* totalWaves: number,
|
|
60
64
|
* phase?: string,
|
|
61
65
|
* openBlockers?: Array<{ reason: string, storyId?: number }>,
|
|
62
66
|
* logger?: { warn?: Function },
|
|
@@ -68,8 +72,6 @@ export async function emitEpicProgress({
|
|
|
68
72
|
epicId,
|
|
69
73
|
done,
|
|
70
74
|
total,
|
|
71
|
-
currentWave,
|
|
72
|
-
totalWaves,
|
|
73
75
|
phase,
|
|
74
76
|
openBlockers = [],
|
|
75
77
|
logger,
|
|
@@ -85,7 +87,7 @@ export async function emitEpicProgress({
|
|
|
85
87
|
blockerCount > 0
|
|
86
88
|
? ` · 🚧 ${blockerCount} blocker${blockerCount === 1 ? '' : 's'}`
|
|
87
89
|
: '';
|
|
88
|
-
const message = `Epic #${epicIdNum} progress ·
|
|
90
|
+
const message = `Epic #${epicIdNum} progress · ${doneN}/${totalN} stories done (${pct}%)${blockerSuffix}`;
|
|
89
91
|
|
|
90
92
|
const payload = {
|
|
91
93
|
severity: blockerCount > 0 ? 'high' : 'medium',
|
|
@@ -109,8 +111,6 @@ export async function emitEpicProgress({
|
|
|
109
111
|
pct,
|
|
110
112
|
done: doneN,
|
|
111
113
|
total: totalN,
|
|
112
|
-
currentWave,
|
|
113
|
-
totalWaves,
|
|
114
114
|
phase,
|
|
115
115
|
openBlockers: openBlockers ?? [],
|
|
116
116
|
},
|
|
@@ -121,11 +121,14 @@ export async function emitEpicProgress({
|
|
|
121
121
|
* Fire a curated `epic-started` webhook event at /deliver kickoff.
|
|
122
122
|
* The Slack consumer anchors the rest of the epic narrative to this fire.
|
|
123
123
|
* Failures are swallowed.
|
|
124
|
+
*
|
|
125
|
+
* Story #4155 — the ready-set runtime has no wave count, so the wave
|
|
126
|
+
* segment was dropped from the message (rather than rendered as
|
|
127
|
+
* `undefined wave(s)`); the sole live caller never supplied one.
|
|
124
128
|
*/
|
|
125
129
|
export async function emitEpicStarted({
|
|
126
130
|
notify,
|
|
127
131
|
epicId,
|
|
128
|
-
totalWaves,
|
|
129
132
|
totalStories,
|
|
130
133
|
title,
|
|
131
134
|
logger,
|
|
@@ -133,7 +136,7 @@ export async function emitEpicStarted({
|
|
|
133
136
|
if (typeof notify !== 'function') return null;
|
|
134
137
|
const epicIdNum = Number(epicId);
|
|
135
138
|
if (!Number.isInteger(epicIdNum) || epicIdNum <= 0) return null;
|
|
136
|
-
const message = `Epic #${epicIdNum} started · ${
|
|
139
|
+
const message = `Epic #${epicIdNum} started · ${totalStories} stor${totalStories === 1 ? 'y' : 'ies'}${title ? ` — ${title}` : ''}`;
|
|
137
140
|
try {
|
|
138
141
|
await notify(
|
|
139
142
|
epicIdNum,
|
|
@@ -241,26 +241,29 @@ export async function reconcileStoryFromGitHub({ provider, storyId } = {}) {
|
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
243
|
* Render a single friction-comment body listing every malformed sub-agent
|
|
244
|
-
* return for a
|
|
245
|
-
* can pin the body shape.
|
|
244
|
+
* return for a recorder beat. Pure helper — no provider call. Exposed so
|
|
245
|
+
* tests can pin the body shape.
|
|
246
|
+
*
|
|
247
|
+
* Story #4155 — under the ready-set runtime there is no wave index; the
|
|
248
|
+
* recorder records the Stories it was handed, so the body is keyed by Epic
|
|
249
|
+
* only.
|
|
246
250
|
*
|
|
247
251
|
* @param {{
|
|
248
252
|
* epicId: number,
|
|
249
|
-
* wave: number,
|
|
250
253
|
* failures: Array<{ storyId: number, error: string, returnText: string }>,
|
|
251
254
|
* }} args
|
|
252
255
|
* @returns {string}
|
|
253
256
|
*/
|
|
254
|
-
export function renderMalformedReturnsFriction({ epicId,
|
|
257
|
+
export function renderMalformedReturnsFriction({ epicId, failures }) {
|
|
255
258
|
const lines = [
|
|
256
|
-
`### 🚧 epic-execute friction — Epic #${epicId}
|
|
259
|
+
`### 🚧 epic-execute friction — Epic #${epicId}`,
|
|
257
260
|
'',
|
|
258
261
|
`**Reason:** \`malformed-subagent-return\``,
|
|
259
262
|
'',
|
|
260
263
|
`${failures.length} sub-agent return(s) did not match the /deliver return contract.`,
|
|
261
264
|
'Each Story below was reconciled from GitHub (labels + `story-run-progress`)',
|
|
262
|
-
'and its
|
|
263
|
-
'`agent::done`.',
|
|
265
|
+
'and its recorded status downgraded to `failed` unless the live ticket',
|
|
266
|
+
'already carried `agent::done`.',
|
|
264
267
|
'',
|
|
265
268
|
];
|
|
266
269
|
for (const f of failures) {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* is a hard block evaluated BEFORE the structured-signal evaluator.
|
|
22
22
|
*
|
|
23
23
|
* Either trigger evaluates the same verdict: if `evaluateAutoMergePredicate`
|
|
24
|
-
* reports `clean: true` (no manual interventions,
|
|
24
|
+
* reports `clean: true` (no manual interventions, every Story done,
|
|
25
25
|
* no story blockers, no critical/high review findings, machine-readable
|
|
26
26
|
* "clean sprint" retro trailer), emit `epic.merge.ready`. Otherwise emit
|
|
27
27
|
* `epic.merge.blocked` with a non-empty reason.
|
|
@@ -172,38 +172,51 @@ function evaluateStateSignals(state, reasons) {
|
|
|
172
172
|
.join('; ')}${interventionCount > 3 ? '; …' : ''}`,
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
// Story #4155 — the ready-set runtime records a flat per-Story status map
|
|
176
|
+
// on the checkpoint (`stories: { [id]: { status, blockerCommentId? } }`)
|
|
177
|
+
// instead of a per-wave `waves[]` history. The clean-run certification
|
|
178
|
+
// reads it directly: a run is clean only when every Story reached `done`
|
|
179
|
+
// and none carries a recorded blocker comment.
|
|
180
|
+
const stories =
|
|
181
|
+
state?.stories && typeof state.stories === 'object' ? state.stories : {};
|
|
182
|
+
const storyStatuses = Object.values(stories).map(
|
|
183
|
+
(s) => s?.status ?? 'pending',
|
|
184
|
+
);
|
|
185
|
+
const nonDoneStatuses = storyStatuses.filter((s) => s !== 'done');
|
|
186
|
+
if (nonDoneStatuses.length > 0) {
|
|
179
187
|
reasons.push(
|
|
180
|
-
`${
|
|
188
|
+
`${nonDoneStatuses.length} story(ies) not done (statuses: ${nonDoneStatuses.join(', ')})`,
|
|
181
189
|
);
|
|
182
190
|
}
|
|
183
|
-
const storyBlockers = countStoryBlockers(
|
|
191
|
+
const storyBlockers = countStoryBlockers(stories);
|
|
184
192
|
if (storyBlockers > 0) {
|
|
185
193
|
reasons.push(
|
|
186
194
|
`${storyBlockers} story-level blocker(s) recorded in run-state`,
|
|
187
195
|
);
|
|
188
196
|
}
|
|
189
|
-
return { interventionCount,
|
|
197
|
+
return { interventionCount, storyStatuses, storyBlockers };
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Count blockers in the flat per-Story `stories` status map: each Story with
|
|
202
|
+
* a recorded `blockerCommentId` and each Story whose status is not `done`
|
|
203
|
+
* contributes one blocker (matching the prior per-wave count semantics).
|
|
204
|
+
*
|
|
205
|
+
* @param {Record<string, { status?: string, blockerCommentId?: string }>} stories
|
|
206
|
+
* @returns {number}
|
|
207
|
+
*/
|
|
208
|
+
function countStoryBlockers(stories) {
|
|
193
209
|
let blockers = 0;
|
|
194
|
-
for (const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (s?.status && s.status !== 'done') {
|
|
205
|
-
blockers += 1;
|
|
206
|
-
}
|
|
210
|
+
for (const s of Object.values(stories ?? {})) {
|
|
211
|
+
if (
|
|
212
|
+
s &&
|
|
213
|
+
typeof s.blockerCommentId === 'string' &&
|
|
214
|
+
s.blockerCommentId.length > 0
|
|
215
|
+
) {
|
|
216
|
+
blockers += 1;
|
|
217
|
+
}
|
|
218
|
+
if (s?.status && s.status !== 'done') {
|
|
219
|
+
blockers += 1;
|
|
207
220
|
}
|
|
208
221
|
}
|
|
209
222
|
return blockers;
|
|
@@ -271,7 +284,7 @@ function evaluateRetroSignals(retro, reasons) {
|
|
|
271
284
|
* reasons: string[],
|
|
272
285
|
* signals: {
|
|
273
286
|
* manualInterventions: number,
|
|
274
|
-
*
|
|
287
|
+
* storyStatuses: string[],
|
|
275
288
|
* storyBlockers: number,
|
|
276
289
|
* severity: { critical: number|null, high: number|null, medium: number|null, suggestion: number|null },
|
|
277
290
|
* retroCompact: boolean,
|
|
@@ -292,7 +305,7 @@ export function deriveAutoMergeVerdict({ state, codeReview, retro }) {
|
|
|
292
305
|
reasons,
|
|
293
306
|
signals: {
|
|
294
307
|
manualInterventions: stateSig.interventionCount,
|
|
295
|
-
|
|
308
|
+
storyStatuses: stateSig.storyStatuses,
|
|
296
309
|
storyBlockers: stateSig.storyBlockers,
|
|
297
310
|
severity: reviewSig.severity,
|
|
298
311
|
retroCompact: retroSig.retroCompact,
|
|
@@ -139,6 +139,12 @@ function buildStoryOnlyManifest(stories, epicId) {
|
|
|
139
139
|
type: 'story',
|
|
140
140
|
branchName: getStoryBranch(epicId, story.id),
|
|
141
141
|
earliestWave,
|
|
142
|
+
// Carry the resolved cross-Story dependency edges on the entry so the
|
|
143
|
+
// presentation layer can derive grouping depth at render time via
|
|
144
|
+
// `assignLayers` (Story #4157) instead of trusting the persisted
|
|
145
|
+
// `earliestWave`. This is the same `explicitStoryDeps` set the wave
|
|
146
|
+
// computation consumed, already closed over the scheduled Story set.
|
|
147
|
+
dependsOn: explicitStoryDeps.get(story.id) ?? [],
|
|
142
148
|
tasks: [],
|
|
143
149
|
};
|
|
144
150
|
});
|
|
@@ -415,6 +415,94 @@ function shellEscape(s) {
|
|
|
415
415
|
return `'${str.replace(/'/g, `'\\''`)}'`;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Render the body lines (everything below a section heading and its trailing
|
|
420
|
+
* blank) for a "proposed issues" bucket — the consumer and framework sections
|
|
421
|
+
* share this shape. Empty buckets collapse to a single `_None._`; populated
|
|
422
|
+
* buckets emit one fenced `gh issue create` stanza per item.
|
|
423
|
+
*
|
|
424
|
+
* @param {object[]} items
|
|
425
|
+
* @returns {string[]}
|
|
426
|
+
*/
|
|
427
|
+
function renderIssueBucket(items) {
|
|
428
|
+
if (items.length === 0) return ['_None._'];
|
|
429
|
+
const lines = [];
|
|
430
|
+
for (const item of items) {
|
|
431
|
+
lines.push(`- **${item.title ?? item.category}**`);
|
|
432
|
+
lines.push('');
|
|
433
|
+
lines.push('```sh');
|
|
434
|
+
lines.push(String(item.command ?? ''));
|
|
435
|
+
lines.push('```');
|
|
436
|
+
lines.push('');
|
|
437
|
+
}
|
|
438
|
+
return lines;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Render the body lines for the "proposed memory updates" bucket — a plain
|
|
443
|
+
* instruction prelude followed by one bullet per insight, or `_None._` when
|
|
444
|
+
* empty. Deliberately NOT YAML frontmatter (asserted by the routed-sections
|
|
445
|
+
* contract test).
|
|
446
|
+
*
|
|
447
|
+
* @param {object[]} items
|
|
448
|
+
* @returns {string[]}
|
|
449
|
+
*/
|
|
450
|
+
function renderMemoryBucket(items) {
|
|
451
|
+
if (items.length === 0) return ['_None._'];
|
|
452
|
+
return [
|
|
453
|
+
'update your memory with the following insights:',
|
|
454
|
+
'',
|
|
455
|
+
...items.map((m) => `- ${m.insight}`),
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Render the body lines for the "one-off / discarded" bucket — one bullet per
|
|
461
|
+
* discarded class naming its occurrence count and source, or `_None._`.
|
|
462
|
+
*
|
|
463
|
+
* @param {object[]} items
|
|
464
|
+
* @returns {string[]}
|
|
465
|
+
*/
|
|
466
|
+
function renderDiscardedBucket(items) {
|
|
467
|
+
if (items.length === 0) return ['_None._'];
|
|
468
|
+
return items.map(
|
|
469
|
+
(d) =>
|
|
470
|
+
`- \`${d.category}\` (${d.occurrences ?? 1} occurrence, source: ${d.source ?? 'consumer'})`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Descriptor table for the four routed-proposal sections, in deterministic
|
|
476
|
+
* emit order (consumer → framework → memory → discarded). Each descriptor
|
|
477
|
+
* pairs a heading, the `routedProposals` field it reads, and a body renderer.
|
|
478
|
+
* {@link renderRoutedSections} walks the table once, so reordering or adding a
|
|
479
|
+
* section is a data edit here rather than another copy-pasted emit block.
|
|
480
|
+
*
|
|
481
|
+
* @type {Array<{ heading: string, field: string, renderBucket: (items: object[]) => string[] }>}
|
|
482
|
+
*/
|
|
483
|
+
const ROUTED_SECTIONS = [
|
|
484
|
+
{
|
|
485
|
+
heading: '### Proposed issues — consumer repo',
|
|
486
|
+
field: 'consumer',
|
|
487
|
+
renderBucket: renderIssueBucket,
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
heading: '### Proposed issues — framework repo',
|
|
491
|
+
field: 'framework',
|
|
492
|
+
renderBucket: renderIssueBucket,
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
heading: '### Proposed memory updates',
|
|
496
|
+
field: 'memory',
|
|
497
|
+
renderBucket: renderMemoryBucket,
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
heading: '### One-off / discarded',
|
|
501
|
+
field: 'discarded',
|
|
502
|
+
renderBucket: renderDiscardedBucket,
|
|
503
|
+
},
|
|
504
|
+
];
|
|
505
|
+
|
|
418
506
|
/**
|
|
419
507
|
* Pure: render the four routed-proposal sections in deterministic order.
|
|
420
508
|
* Returns `null` when `routedProposals` is absent or fully empty — the
|
|
@@ -432,80 +520,23 @@ function renderRoutedSections(routedProposals) {
|
|
|
432
520
|
) {
|
|
433
521
|
return null;
|
|
434
522
|
}
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
? routedProposals.memory
|
|
443
|
-
: [];
|
|
444
|
-
const discarded = Array.isArray(routedProposals.discarded)
|
|
445
|
-
? routedProposals.discarded
|
|
446
|
-
: [];
|
|
447
|
-
if (
|
|
448
|
-
framework.length === 0 &&
|
|
449
|
-
consumer.length === 0 &&
|
|
450
|
-
memory.length === 0 &&
|
|
451
|
-
discarded.length === 0
|
|
452
|
-
) {
|
|
523
|
+
const buckets = ROUTED_SECTIONS.map((section) => {
|
|
524
|
+
const items = Array.isArray(routedProposals[section.field])
|
|
525
|
+
? routedProposals[section.field]
|
|
526
|
+
: [];
|
|
527
|
+
return { section, items };
|
|
528
|
+
});
|
|
529
|
+
if (buckets.every(({ items }) => items.length === 0)) {
|
|
453
530
|
return null;
|
|
454
531
|
}
|
|
455
532
|
|
|
533
|
+
// Each section renders as `[heading, '', ...body]`; a single blank-line
|
|
534
|
+
// separator sits between consecutive sections (no trailing separator after
|
|
535
|
+
// the last), reproducing the original hand-unrolled push sequence exactly.
|
|
456
536
|
const out = [];
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
out.push('_None._');
|
|
461
|
-
} else {
|
|
462
|
-
for (const item of consumer) {
|
|
463
|
-
out.push(`- **${item.title ?? item.category}**`);
|
|
464
|
-
out.push('');
|
|
465
|
-
out.push('```sh');
|
|
466
|
-
out.push(String(item.command ?? ''));
|
|
467
|
-
out.push('```');
|
|
468
|
-
out.push('');
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
out.push('');
|
|
472
|
-
out.push('### Proposed issues — framework repo');
|
|
473
|
-
out.push('');
|
|
474
|
-
if (framework.length === 0) {
|
|
475
|
-
out.push('_None._');
|
|
476
|
-
} else {
|
|
477
|
-
for (const item of framework) {
|
|
478
|
-
out.push(`- **${item.title ?? item.category}**`);
|
|
479
|
-
out.push('');
|
|
480
|
-
out.push('```sh');
|
|
481
|
-
out.push(String(item.command ?? ''));
|
|
482
|
-
out.push('```');
|
|
483
|
-
out.push('');
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
out.push('');
|
|
487
|
-
out.push('### Proposed memory updates');
|
|
488
|
-
out.push('');
|
|
489
|
-
if (memory.length === 0) {
|
|
490
|
-
out.push('_None._');
|
|
491
|
-
} else {
|
|
492
|
-
out.push('update your memory with the following insights:');
|
|
493
|
-
out.push('');
|
|
494
|
-
for (const m of memory) {
|
|
495
|
-
out.push(`- ${m.insight}`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
out.push('');
|
|
499
|
-
out.push('### One-off / discarded');
|
|
500
|
-
out.push('');
|
|
501
|
-
if (discarded.length === 0) {
|
|
502
|
-
out.push('_None._');
|
|
503
|
-
} else {
|
|
504
|
-
for (const d of discarded) {
|
|
505
|
-
out.push(
|
|
506
|
-
`- \`${d.category}\` (${d.occurrences ?? 1} occurrence, source: ${d.source ?? 'consumer'})`,
|
|
507
|
-
);
|
|
508
|
-
}
|
|
537
|
+
for (const { section, items } of buckets) {
|
|
538
|
+
if (out.length > 0) out.push('');
|
|
539
|
+
out.push(section.heading, '', ...section.renderBucket(items));
|
|
509
540
|
}
|
|
510
541
|
return out;
|
|
511
542
|
}
|
|
@@ -157,6 +157,45 @@ function sanitizeLabels(labels) {
|
|
|
157
157
|
return out.length > 0 ? out : undefined;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Descriptor table for the structured-body → markdown projection, in
|
|
162
|
+
* canonical emit order (`## Goal`, `## Changes`, `## Acceptance`,
|
|
163
|
+
* `## Verify`). Each descriptor reads one body field and returns the
|
|
164
|
+
* section's markdown block when the field is present and non-empty, or `null`
|
|
165
|
+
* to omit it. Adding a section is a one-line data edit rather than a new
|
|
166
|
+
* branch in {@link renderBody}.
|
|
167
|
+
*
|
|
168
|
+
* @type {Array<{ field: string, render: (value: unknown) => string | null }>}
|
|
169
|
+
*/
|
|
170
|
+
const SPEC_BODY_SECTIONS = [
|
|
171
|
+
{
|
|
172
|
+
field: 'goal',
|
|
173
|
+
render: (goal) =>
|
|
174
|
+
typeof goal === 'string' && goal.length > 0 ? `## Goal\n${goal}` : null,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
field: 'changes',
|
|
178
|
+
render: (changes) =>
|
|
179
|
+
Array.isArray(changes) && changes.length > 0
|
|
180
|
+
? `## Changes\n${changes.map((c) => `- ${String(c)}`).join('\n')}`
|
|
181
|
+
: null,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
field: 'acceptance',
|
|
185
|
+
render: (acceptance) =>
|
|
186
|
+
Array.isArray(acceptance) && acceptance.length > 0
|
|
187
|
+
? `## Acceptance\n${acceptance.map((a) => `- [ ] ${String(a)}`).join('\n')}`
|
|
188
|
+
: null,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
field: 'verify',
|
|
192
|
+
render: (verify) =>
|
|
193
|
+
Array.isArray(verify) && verify.length > 0
|
|
194
|
+
? `## Verify\n${verify.map((v) => `- ${String(v)}`).join('\n')}`
|
|
195
|
+
: null,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
160
199
|
/**
|
|
161
200
|
* Convert a decomposer body value into a spec `body` string. The
|
|
162
201
|
* decomposer schema admits two shapes for a Story body:
|
|
@@ -190,20 +229,9 @@ function renderBody(body) {
|
|
|
190
229
|
if (typeof body !== 'object') return undefined;
|
|
191
230
|
|
|
192
231
|
const sections = [];
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (Array.isArray(body.changes) && body.changes.length > 0) {
|
|
197
|
-
const items = body.changes.map((c) => `- ${String(c)}`).join('\n');
|
|
198
|
-
sections.push(`## Changes\n${items}`);
|
|
199
|
-
}
|
|
200
|
-
if (Array.isArray(body.acceptance) && body.acceptance.length > 0) {
|
|
201
|
-
const items = body.acceptance.map((a) => `- [ ] ${String(a)}`).join('\n');
|
|
202
|
-
sections.push(`## Acceptance\n${items}`);
|
|
203
|
-
}
|
|
204
|
-
if (Array.isArray(body.verify) && body.verify.length > 0) {
|
|
205
|
-
const items = body.verify.map((v) => `- ${String(v)}`).join('\n');
|
|
206
|
-
sections.push(`## Verify\n${items}`);
|
|
232
|
+
for (const descriptor of SPEC_BODY_SECTIONS) {
|
|
233
|
+
const block = descriptor.render(body[descriptor.field]);
|
|
234
|
+
if (block !== null) sections.push(block);
|
|
207
235
|
}
|
|
208
236
|
return sections.length > 0 ? sections.join('\n\n') : undefined;
|
|
209
237
|
}
|
|
@@ -56,6 +56,9 @@ import { parseLedger } from './lifecycle/trace-logger.js';
|
|
|
56
56
|
* coordinated under a shared identity) and no assignee PATCH ever writes a
|
|
57
57
|
* literal `[USERNAME]` (HTTP 422).
|
|
58
58
|
*/
|
|
59
|
+
// kept (dead-export allowlist): public config sentinel — the distributed
|
|
60
|
+
// `.agentrc.json` / templates carry this literal; exported so consumers and
|
|
61
|
+
// future call sites resolve it by symbol rather than re-typing the string.
|
|
59
62
|
export const OPERATOR_HANDLE_PLACEHOLDER = '@[USERNAME]';
|
|
60
63
|
const OPERATOR_HANDLE_PLACEHOLDER_BARE = '[USERNAME]';
|
|
61
64
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Sizing model (Story #3760 — profile-matrix collapse; Story #3874 — one
|
|
20
20
|
* uniform relaxed profile):
|
|
21
|
-
* - Flat knobs: `softFiles` (~
|
|
21
|
+
* - Flat knobs: `softFiles` (~15), `hardFiles` (~30), `maxAcceptance` (~14),
|
|
22
22
|
* `softAcceptanceCount` (~10). No per-profile ceiling map, no parallel
|
|
23
23
|
* `testSurface` axis, no selector and no second profile.
|
|
24
24
|
* - The four-profile `sizingProfile` enum is replaced by a single optional
|
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
|
|
33
33
|
export const DEFAULT_TASK_SIZING = Object.freeze({
|
|
34
34
|
// Typical-Story warning thresholds (soft — emit advisory findings).
|
|
35
|
-
softFiles:
|
|
35
|
+
// Story #4162 raised `softFiles` 8 → 15: a capability-sized Story routinely
|
|
36
|
+
// touches a dozen-plus files for one cohesive reason, so the advisory width
|
|
37
|
+
// nudge fired far too eagerly and biased the decomposer toward over-slicing.
|
|
38
|
+
// The hard `hardFiles` rejection (30) is unchanged.
|
|
39
|
+
softFiles: 15,
|
|
36
40
|
softAcceptanceCount: 10,
|
|
37
41
|
// Hard ceilings (rejection unless lifted).
|
|
38
42
|
hardFiles: 30,
|