mandrel 2.53.0 → 2.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +4 -2
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +158 -7
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +68 -5
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +60 -114
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +19 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/findings/route-finding.js +4 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +241 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +24 -1
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +119 -6
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +59 -38
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +70 -6
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +40 -16
- package/.agents/workflows/helpers/deliver-story-reference.md +7 -4
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +8 -1
- package/.agents/workflows/mandrel-plan.md +4 -7
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +34 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { rm } from 'node:fs/promises';
|
|
41
|
-
import { getLimits, PROJECT_ROOT } from '../../config-resolver.js';
|
|
41
|
+
import { getLimits, getPaths, PROJECT_ROOT } from '../../config-resolver.js';
|
|
42
42
|
import { gitSpawn } from '../../git-utils.js';
|
|
43
43
|
import { Logger } from '../../Logger.js';
|
|
44
44
|
import { sweepTempRetention } from '../../temp-retention.js';
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
PLAN_SUMMARY_COMMENT_TYPE,
|
|
89
89
|
} from './summary.js';
|
|
90
90
|
import { closeSupersededTickets } from './supersede-ops.js';
|
|
91
|
+
import { predictWaveSerialisation } from './wave-serialisation.js';
|
|
91
92
|
|
|
92
93
|
/** Checkpoint schema version written on each Story's story-plan-state. */
|
|
93
94
|
const PLAN_CHECKPOINT_SCHEMA_VERSION_V2 = 2;
|
|
@@ -212,6 +213,7 @@ async function runSupersedePhase(args) {
|
|
|
212
213
|
reason: `phase-error: ${err.message}`,
|
|
213
214
|
closed: [],
|
|
214
215
|
planned: [],
|
|
216
|
+
epicRollup: { closed: [], pending: [] },
|
|
215
217
|
skipped: [],
|
|
216
218
|
failed: (args.sourceTicketIds ?? []).map((ticket) => ({
|
|
217
219
|
ticket,
|
|
@@ -831,6 +833,17 @@ export async function runPlanPersist({
|
|
|
831
833
|
})),
|
|
832
834
|
);
|
|
833
835
|
|
|
836
|
+
// Story #5265: the table says which Stories share an order; the dispatcher
|
|
837
|
+
// decides which of those actually run together, and it decides on the
|
|
838
|
+
// evidence-widened footprint. Run its own predicate over the assembled
|
|
839
|
+
// bodies — the exact artifact the tick will read back off GitHub — so the
|
|
840
|
+
// comment names the serialisation instead of promising parallelism the
|
|
841
|
+
// next tick refuses. `tempRoot` is threaded for the same reason the tick
|
|
842
|
+
// threads it: the scrape must ignore this project's scratch root.
|
|
843
|
+
const waveCollisions = predictWaveSerialisation(waveTable, stories, {
|
|
844
|
+
tempRoot: getPaths(config).tempRoot,
|
|
845
|
+
});
|
|
846
|
+
|
|
834
847
|
// Story #4541: `readPlanMetrics` is declared `(epicId, config)` but was
|
|
835
848
|
// called with `config` first, so the ledger path resolver received the
|
|
836
849
|
// config object as an `epicId` and threw its guard on every single run —
|
|
@@ -860,6 +873,7 @@ export async function runPlanPersist({
|
|
|
860
873
|
// collisions the conflict passes found belong on the same surface, or the
|
|
861
874
|
// promise is the only half anyone reads.
|
|
862
875
|
conflictFindings: assembledConflicts,
|
|
876
|
+
waveCollisions,
|
|
863
877
|
});
|
|
864
878
|
|
|
865
879
|
if (!dryRun) {
|
|
@@ -890,6 +904,10 @@ export async function runPlanPersist({
|
|
|
890
904
|
stories,
|
|
891
905
|
created,
|
|
892
906
|
sourceTicketIds,
|
|
907
|
+
// Story #5280 — closing a source ticket is a child state change, so the
|
|
908
|
+
// phase re-derives the container above it. It needs the config the rollup
|
|
909
|
+
// reads its board and operator handle from.
|
|
910
|
+
config,
|
|
893
911
|
dryRun,
|
|
894
912
|
closeSuperseded,
|
|
895
913
|
});
|
|
@@ -915,6 +933,11 @@ export async function runPlanPersist({
|
|
|
915
933
|
reachability,
|
|
916
934
|
freshness,
|
|
917
935
|
waveTable,
|
|
936
|
+
// Story #5265 AC-2/AC-4: both halves of what persist concluded but used
|
|
937
|
+
// to keep to itself — the `refactors-existing` declarations it rewrote,
|
|
938
|
+
// and the same-order pairs the dispatcher will serialize.
|
|
939
|
+
assumptionNormalizations: validated.normalizations ?? [],
|
|
940
|
+
waveCollisions,
|
|
918
941
|
supersede,
|
|
919
942
|
epic: containerEpic,
|
|
920
943
|
};
|
|
@@ -1098,6 +1098,11 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1098
1098
|
id,
|
|
1099
1099
|
title: story.title,
|
|
1100
1100
|
url: result.url,
|
|
1101
|
+
// Kept, not discarded (Story #5280). `createIssue` hands back the
|
|
1102
|
+
// database id, which is the only identifier the native sub-issue write
|
|
1103
|
+
// accepts — and dropping it here is what made the Epic linker re-read
|
|
1104
|
+
// every Story this run had just created to recover it.
|
|
1105
|
+
internalId: result.internalId,
|
|
1101
1106
|
// True when the provider's retry probe adopted an issue a lost-response
|
|
1102
1107
|
// first attempt had already filed — pre-existing either way.
|
|
1103
1108
|
adopted: result.adopted === true,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { computeStoryWaves } from '../dependency-analyzer.js';
|
|
17
|
+
import { renderPredictedSerialisationLines } from './wave-serialisation.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Structured-comment type for the persist summary.
|
|
@@ -129,6 +130,7 @@ export function buildPlanSummaryCommentBody({
|
|
|
129
130
|
planMetricsLine = null,
|
|
130
131
|
stories = null,
|
|
131
132
|
conflictFindings = null,
|
|
133
|
+
waveCollisions = null,
|
|
132
134
|
// legacy unused knobs kept so older test call sites don't crash mid-migration
|
|
133
135
|
single = null,
|
|
134
136
|
amend = null,
|
|
@@ -180,6 +182,7 @@ export function buildPlanSummaryCommentBody({
|
|
|
180
182
|
'#### Delivery order (`depends_on`)',
|
|
181
183
|
'',
|
|
182
184
|
...renderWaveTableLines(waveTable),
|
|
185
|
+
...renderPredictedSerialisationLines(waveCollisions),
|
|
183
186
|
...renderSharedEditorLines(conflictFindings),
|
|
184
187
|
'',
|
|
185
188
|
`_Deliver with \`${deliverCommand}\` — \`/mandrel-deliver\` resolves the dependency graph from live state, so edges may point at Stories from earlier plan runs._`,
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
* partial failure reports which tickets were and were not closed so the
|
|
29
29
|
* operator can finish by hand.
|
|
30
30
|
*
|
|
31
|
+
* The close also strips the source ticket's `agent::*` label (Story #5255).
|
|
32
|
+
* A retired ticket has no agent state, and the one it kept was read as live
|
|
33
|
+
* work: `agent::blocked` is the state a Story must be in to be re-planned, so
|
|
34
|
+
* this path closes exactly the tickets carrying it, and a closed-but-blocked
|
|
35
|
+
* child pinned its container Epic open on every rollup thereafter. The
|
|
36
|
+
* derivation in `ticketing/bulk.js` now ignores closed children's labels too —
|
|
37
|
+
* that half covers the tickets already closed and the ones closed by hand;
|
|
38
|
+
* this one stops new ones being written.
|
|
39
|
+
*
|
|
31
40
|
* Idempotency is keyed off the `superseded-by` structured-comment marker
|
|
32
41
|
* (`upsertStructuredComment`), not a bare `postComment`, so a re-run cannot
|
|
33
42
|
* double-comment.
|
|
@@ -36,10 +45,12 @@
|
|
|
36
45
|
*/
|
|
37
46
|
|
|
38
47
|
import { Logger } from '../../Logger.js';
|
|
48
|
+
import { AGENT_LABELS } from '../../label-constants.js';
|
|
39
49
|
import {
|
|
40
50
|
concurrentMap,
|
|
41
51
|
FANOUT_CONCURRENCY,
|
|
42
52
|
} from '../../util/concurrent-map.js';
|
|
53
|
+
import { rollUpEpicForStory } from '../epic-rollup.js';
|
|
43
54
|
import { upsertStructuredComment } from '../ticketing.js';
|
|
44
55
|
|
|
45
56
|
/** Structured-comment type marking a source issue as superseded. */
|
|
@@ -55,6 +66,40 @@ const SUPERSEDED_BY_COMMENT_TYPE = 'superseded-by';
|
|
|
55
66
|
*/
|
|
56
67
|
export const SUPERSEDE_CLOSE_REASON = 'not_planned';
|
|
57
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Every `agent::*` label, as the set the supersede close strips.
|
|
71
|
+
*
|
|
72
|
+
* A retired ticket has no agent state. `agent::done` would be the wrong
|
|
73
|
+
* substitute — the work was re-planned, never delivered — so the label is
|
|
74
|
+
* removed rather than rewritten, and the ticket ends carrying only its
|
|
75
|
+
* `type::`/domain labels and the supersede comment that explains it.
|
|
76
|
+
*/
|
|
77
|
+
const AGENT_STATE_LABELS = Object.freeze(Object.values(AGENT_LABELS));
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The single `updateTicket` mutation that retires a source ticket.
|
|
81
|
+
*
|
|
82
|
+
* Closing and clearing the state ride one write: two calls could leave the
|
|
83
|
+
* ticket closed but still wearing `agent::blocked`, which is the shape that
|
|
84
|
+
* pinned a container Epic open forever (Story #5255).
|
|
85
|
+
*
|
|
86
|
+
* A ticket with no `agent::*` label gets the bare close, unchanged from before
|
|
87
|
+
* that Story — `updateTicket` merges a `labels` mutation by reading the issue
|
|
88
|
+
* back, so an unconditional empty `remove` would buy a wasted round-trip per
|
|
89
|
+
* superseded ticket. `_ticketSnapshot` feeds that merge the copy
|
|
90
|
+
* `probeSourceTicket` already fetched.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ labels?: unknown }} ticket The probe's fresh copy.
|
|
93
|
+
* @returns {object} Mutations for `provider.updateTicket`.
|
|
94
|
+
*/
|
|
95
|
+
function supersedeCloseMutations(ticket) {
|
|
96
|
+
const close = { state: 'closed', state_reason: SUPERSEDE_CLOSE_REASON };
|
|
97
|
+
const labels = Array.isArray(ticket?.labels) ? ticket.labels : [];
|
|
98
|
+
const remove = AGENT_STATE_LABELS.filter((label) => labels.includes(label));
|
|
99
|
+
if (remove.length === 0) return close;
|
|
100
|
+
return { ...close, labels: { remove }, _ticketSnapshot: ticket };
|
|
101
|
+
}
|
|
102
|
+
|
|
58
103
|
/**
|
|
59
104
|
* Coerce one `supersedes[]` entry into `{ id, note }`.
|
|
60
105
|
*
|
|
@@ -329,13 +374,22 @@ export function buildSupersedeCommentBody({
|
|
|
329
374
|
/**
|
|
330
375
|
* Resolve the live state of a source ticket.
|
|
331
376
|
*
|
|
332
|
-
*
|
|
377
|
+
* The ticket itself rides along so the close can strip the `agent::*` label
|
|
378
|
+
* without a second read: `updateTicket`'s label merge takes a
|
|
379
|
+
* `_ticketSnapshot` for exactly this, and this probe has already paid for the
|
|
380
|
+
* fresh copy.
|
|
381
|
+
*
|
|
382
|
+
* @returns {Promise<{ ok: true, state: string, ticket: object } | { ok: false, reason: string }>}
|
|
333
383
|
*/
|
|
334
384
|
async function probeSourceTicket(provider, id) {
|
|
335
385
|
try {
|
|
336
386
|
const ticket = await provider.getTicket(id, { fresh: true });
|
|
337
387
|
if (!ticket) return { ok: false, reason: 'not-found' };
|
|
338
|
-
return {
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
state: String(ticket.state ?? 'open').toLowerCase(),
|
|
391
|
+
ticket,
|
|
392
|
+
};
|
|
339
393
|
} catch (err) {
|
|
340
394
|
return { ok: false, reason: `inaccessible: ${err.message}` };
|
|
341
395
|
}
|
|
@@ -370,10 +424,7 @@ async function closeOneSupersededTicket({
|
|
|
370
424
|
sourceTicketIds,
|
|
371
425
|
}),
|
|
372
426
|
);
|
|
373
|
-
await provider.updateTicket(id,
|
|
374
|
-
state: 'closed',
|
|
375
|
-
state_reason: SUPERSEDE_CLOSE_REASON,
|
|
376
|
-
});
|
|
427
|
+
await provider.updateTicket(id, supersedeCloseMutations(probe.ticket));
|
|
377
428
|
return { outcome: 'closed' };
|
|
378
429
|
} catch (err) {
|
|
379
430
|
return { outcome: 'failed', reason: err.message };
|
|
@@ -394,6 +445,9 @@ async function closeOneSupersededTicket({
|
|
|
394
445
|
* slug is the only identifier that means anything before the writes land.
|
|
395
446
|
* @property {Array<{ ticket: number, reason: string }>} skipped
|
|
396
447
|
* @property {Array<{ ticket: number, reason: string }>} failed
|
|
448
|
+
* @property {{ closed: number[], pending: number[] }} epicRollup Container
|
|
449
|
+
* Epics this phase's closes resolved. Empty on a dry run and on a phase that
|
|
450
|
+
* closed nothing.
|
|
397
451
|
*/
|
|
398
452
|
|
|
399
453
|
function emptyReport(overrides) {
|
|
@@ -405,10 +459,53 @@ function emptyReport(overrides) {
|
|
|
405
459
|
planned: [],
|
|
406
460
|
skipped: [],
|
|
407
461
|
failed: [],
|
|
462
|
+
epicRollup: { closed: [], pending: [] },
|
|
408
463
|
...overrides,
|
|
409
464
|
};
|
|
410
465
|
}
|
|
411
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Re-derive the container Epic above every ticket this phase just closed.
|
|
469
|
+
*
|
|
470
|
+
* Closing a source ticket is a child state change like any other, and it was
|
|
471
|
+
* the one edge with no rollup behind it. Superseding a cohort therefore left
|
|
472
|
+
* its container open indefinitely: every child was closed, so no delivery
|
|
473
|
+
* would ever run and no land tail would ever fire the derivation. The Epic sat
|
|
474
|
+
* open above finished work until someone noticed.
|
|
475
|
+
*
|
|
476
|
+
* Runs **after** the closes land, never alongside them: the rollup reads each
|
|
477
|
+
* child's state back, so racing it against the writes it is meant to observe
|
|
478
|
+
* would derive from a tree half of which has not been written yet.
|
|
479
|
+
*
|
|
480
|
+
* Sequential, sharing one `skipEpicIds` set, because siblings share a
|
|
481
|
+
* container — without it the second ticket re-derives, and re-closes, the Epic
|
|
482
|
+
* the first already closed. `rollUpEpicForStory` never throws, so no guard is
|
|
483
|
+
* needed here beyond the phase-level one the caller already holds.
|
|
484
|
+
*
|
|
485
|
+
* @param {{ closedIds: number[], provider: object, config?: object }} opts
|
|
486
|
+
* @returns {Promise<{ closed: number[], pending: number[] }>}
|
|
487
|
+
*/
|
|
488
|
+
async function rollUpContainersFor({ closedIds, provider, config }) {
|
|
489
|
+
const seen = new Set();
|
|
490
|
+
const closed = new Set();
|
|
491
|
+
const pending = new Set();
|
|
492
|
+
for (const storyId of closedIds) {
|
|
493
|
+
const outcome = await rollUpEpicForStory({
|
|
494
|
+
storyId,
|
|
495
|
+
provider,
|
|
496
|
+
config,
|
|
497
|
+
skipEpicIds: seen,
|
|
498
|
+
});
|
|
499
|
+
for (const epic of outcome.epics) seen.add(epic.epicId);
|
|
500
|
+
for (const epicId of outcome.closed) closed.add(epicId);
|
|
501
|
+
for (const epicId of outcome.pending) pending.add(epicId);
|
|
502
|
+
}
|
|
503
|
+
// A container reported pending by one ticket's rollup and closed by a later
|
|
504
|
+
// one is closed: the last answer saw the whole cohort.
|
|
505
|
+
for (const epicId of closed) pending.delete(epicId);
|
|
506
|
+
return { closed: [...closed], pending: [...pending] };
|
|
507
|
+
}
|
|
508
|
+
|
|
412
509
|
/**
|
|
413
510
|
* Comment on and close every superseded source ticket.
|
|
414
511
|
*
|
|
@@ -421,6 +518,7 @@ function emptyReport(overrides) {
|
|
|
421
518
|
* @param {Array<{ slug: string, supersedes: Array<{ id: number, note: string|null }> }>} args.stories
|
|
422
519
|
* @param {Array<{ slug: string, id: number, title: string }>} args.created
|
|
423
520
|
* @param {number[]} args.sourceTicketIds
|
|
521
|
+
* @param {object} [args.config] Resolved `.agentrc.json`, for the Epic rollup.
|
|
424
522
|
* @param {boolean} [args.dryRun=false]
|
|
425
523
|
* @param {boolean} [args.closeSuperseded=true]
|
|
426
524
|
* @returns {Promise<SupersedeReport>}
|
|
@@ -430,6 +528,7 @@ export async function closeSupersededTickets({
|
|
|
430
528
|
stories,
|
|
431
529
|
created,
|
|
432
530
|
sourceTicketIds,
|
|
531
|
+
config,
|
|
433
532
|
dryRun = false,
|
|
434
533
|
closeSuperseded = true,
|
|
435
534
|
}) {
|
|
@@ -482,6 +581,14 @@ export async function closeSupersededTickets({
|
|
|
482
581
|
recordSupersedeOutcome(report, units[index], result);
|
|
483
582
|
});
|
|
484
583
|
|
|
584
|
+
if (report.closed.length > 0) {
|
|
585
|
+
report.epicRollup = await rollUpContainersFor({
|
|
586
|
+
closedIds: report.closed,
|
|
587
|
+
provider,
|
|
588
|
+
config,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
485
592
|
logSupersedeReport(report);
|
|
486
593
|
return report;
|
|
487
594
|
}
|
|
@@ -563,4 +670,10 @@ function logSupersedeReport(report) {
|
|
|
563
670
|
'close it by hand.',
|
|
564
671
|
);
|
|
565
672
|
}
|
|
673
|
+
if (report.epicRollup.closed.length > 0) {
|
|
674
|
+
Logger.info(
|
|
675
|
+
`[plan-persist] container Epic(s) closed by the supersede rollup: ` +
|
|
676
|
+
`${report.epicRollup.closed.map((id) => `#${id}`).join(', ')}.`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
566
679
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wave-serialisation.js — what the dispatcher will actually do with the wave
|
|
3
|
+
* table plan-persist prints (Story #5265).
|
|
4
|
+
*
|
|
5
|
+
* Kept out of `summary.js` because it answers a different question. The
|
|
6
|
+
* summary module renders receipts persist already computed; this one runs the
|
|
7
|
+
* *runtime's* collision predicate over the assembled Story bodies to work out
|
|
8
|
+
* which of the table's promises the next tick will refuse to keep.
|
|
9
|
+
*
|
|
10
|
+
* @module lib/orchestration/plan-persist/wave-serialisation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
detectCollision,
|
|
15
|
+
OVERLAP_SOURCES,
|
|
16
|
+
renderScrapeAttribution,
|
|
17
|
+
} from '../../wave-runner/footprint.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Predict which same-wave pairs the dispatch guard will actually refuse to
|
|
21
|
+
* co-dispatch (Story #5265).
|
|
22
|
+
*
|
|
23
|
+
* The wave table answers a `depends_on` question, and the runtime answers a
|
|
24
|
+
* different one: `stories-wave-tick.js` withholds on {@link detectCollision},
|
|
25
|
+
* whose footprint is the declared `changes[]` **plus** every path scraped out
|
|
26
|
+
* of the Story's title, spec and serialized body — and that body carries
|
|
27
|
+
* `## Verify`, so two Stories that merely run the same gate script share a
|
|
28
|
+
* path neither will edit. The table therefore promised parallelism the very
|
|
29
|
+
* next tick refused, with nothing anywhere reconciling the two.
|
|
30
|
+
*
|
|
31
|
+
* This runs the runtime's own exported predicate — not a reimplementation of
|
|
32
|
+
* it — pairwise within each wave, so the prediction cannot drift from the
|
|
33
|
+
* behaviour it predicts.
|
|
34
|
+
*
|
|
35
|
+
* @param {ReturnType<typeof buildWaveTable>} waveTable
|
|
36
|
+
* @param {Array<{ slug: string, title?: string, body?: string, spec?: string, changes?: Array }>} stories
|
|
37
|
+
* @param {{ tempRoot?: string }} [options]
|
|
38
|
+
* @returns {Array<{ wave: number, slugs: [string, string], paths: string[], source: string, attribution: object[] }>}
|
|
39
|
+
*/
|
|
40
|
+
export function predictWaveSerialisation(waveTable, stories, options = {}) {
|
|
41
|
+
const bySlug = new Map(
|
|
42
|
+
(Array.isArray(stories) ? stories : []).map((s) => [s.slug, s]),
|
|
43
|
+
);
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const { wave, stories: members } of Array.isArray(waveTable)
|
|
46
|
+
? waveTable
|
|
47
|
+
: []) {
|
|
48
|
+
for (let i = 0; i < members.length; i += 1) {
|
|
49
|
+
for (let j = i + 1; j < members.length; j += 1) {
|
|
50
|
+
const a = bySlug.get(members[i].slug);
|
|
51
|
+
const b = bySlug.get(members[j].slug);
|
|
52
|
+
if (!a || !b) continue;
|
|
53
|
+
const collision = detectCollision(a, b, options);
|
|
54
|
+
if (collision) {
|
|
55
|
+
out.push({
|
|
56
|
+
wave,
|
|
57
|
+
slugs: [members[i].slug, members[j].slug],
|
|
58
|
+
...collision,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render the predicted serialisation beside the wave table (Story #5265).
|
|
69
|
+
*
|
|
70
|
+
* Same shape as {@link renderSharedEditorLines} on purpose: both are caveats
|
|
71
|
+
* against the same promise, and an operator should read them the same way.
|
|
72
|
+
* The difference is what they know — the shared-editor pass names paths two
|
|
73
|
+
* Stories both *write*, this one names every pair the dispatcher will refuse
|
|
74
|
+
* to run together whatever the reason, including the pairs whose only shared
|
|
75
|
+
* path was scraped out of a `## Verify` line.
|
|
76
|
+
*
|
|
77
|
+
* @param {ReturnType<typeof predictWaveSerialisation>} collisions
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
export function renderPredictedSerialisationLines(collisions) {
|
|
81
|
+
const list = Array.isArray(collisions) ? collisions : [];
|
|
82
|
+
if (list.length === 0) return [];
|
|
83
|
+
const rows = list.map((c) => {
|
|
84
|
+
const scraped = renderScrapeAttribution(c.attribution);
|
|
85
|
+
return `| \`${c.slugs[0]}\` + \`${c.slugs[1]}\` | ${c.paths
|
|
86
|
+
.map((p) => `\`${p}\``)
|
|
87
|
+
.join(', ')} | ${c.source} | ${scraped ? `\`${scraped}\`` : '—'} |`;
|
|
88
|
+
});
|
|
89
|
+
const scrapedOnly = list.filter(
|
|
90
|
+
(c) => c.source === OVERLAP_SOURCES.SCRAPED,
|
|
91
|
+
).length;
|
|
92
|
+
return [
|
|
93
|
+
'',
|
|
94
|
+
`#### ⚠️ Predicted serialisation (${list.length} same-order pair(s))`,
|
|
95
|
+
'',
|
|
96
|
+
'| Stories | Colliding paths | Overlap source | Scraped from |',
|
|
97
|
+
'| --- | --- | --- | --- |',
|
|
98
|
+
...rows,
|
|
99
|
+
'',
|
|
100
|
+
'_The dispatcher compares the **evidence-widened** footprint — declared ' +
|
|
101
|
+
'`changes[]` plus every path named in the title, `## Spec` and the rest ' +
|
|
102
|
+
'of the body — so these pairs are shown in one order above but will be ' +
|
|
103
|
+
`dispatched one at a time. ${scrapedOnly} pair(s) collide only on ` +
|
|
104
|
+
'scraped paths; the "Scraped from" column names the field each such ' +
|
|
105
|
+
'path was read out of, so a shared `## Verify` command is ' +
|
|
106
|
+
'distinguishable from a genuine unpredicted edit target. The guard is ' +
|
|
107
|
+
'deliberately not narrowed to `changes[]`: the declaration is a lower ' +
|
|
108
|
+
'bound (Story #4875) and under-serialising is the worse failure._',
|
|
109
|
+
];
|
|
110
|
+
}
|