mandrel 1.57.0 → 1.59.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 +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +241 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +16 -3
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +21 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -41,9 +41,34 @@ import { emitSpawnTimeoutBlockedResult } from './timeout-blocked-emitter.js';
|
|
|
41
41
|
* Run pre-merge gates and, on a clean outcome, the bounded baseline
|
|
42
42
|
* auto-refresh. Returns `{ blocked }` so the locked pipeline can
|
|
43
43
|
* short-circuit on a `blocked` / `blocked-timeout` gate outcome.
|
|
44
|
+
*
|
|
45
|
+
* `runPreMergeValidation` is an injectable seam (Story #3973): the
|
|
46
|
+
* forwarding of `config: ctx.config` into the gate builder is load-bearing
|
|
47
|
+
* — without it the typecheck gate ignores `project.commands.typecheck` and
|
|
48
|
+
* falls back to the hardcoded `npm run typecheck`, blocking every close for
|
|
49
|
+
* any consumer with a non-default typecheck command. The default is the
|
|
50
|
+
* module-level `runPreMergeValidation`; tests inject a spy to pin that
|
|
51
|
+
* `config` reaches the gate builder through this call site.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} ctx - The locked-pipeline context bundle.
|
|
54
|
+
* @param {object} [deps] - Injectable collaborators.
|
|
55
|
+
* @param {typeof runPreMergeValidation} [deps.runPreMergeValidationFn]
|
|
56
|
+
* @param {typeof runAutoRefreshSafely} [deps.runAutoRefreshSafelyFn]
|
|
44
57
|
*/
|
|
45
|
-
async function runGatesAndRefresh(
|
|
46
|
-
|
|
58
|
+
export async function runGatesAndRefresh(
|
|
59
|
+
ctx,
|
|
60
|
+
{
|
|
61
|
+
runPreMergeValidationFn = runPreMergeValidation,
|
|
62
|
+
runAutoRefreshSafelyFn = runAutoRefreshSafely,
|
|
63
|
+
} = {},
|
|
64
|
+
) {
|
|
65
|
+
// Story #4017 — one shared cycle-state object per close cycle. The
|
|
66
|
+
// gate-failure attribution retry and the post-gates auto-refresh both
|
|
67
|
+
// funnel through `runRefreshCommit`, which keys its idempotency token
|
|
68
|
+
// off this object so each baseline kind is refreshed (scored +
|
|
69
|
+
// committed) at most once per close.
|
|
70
|
+
const cycleState = { refreshedKinds: new Set(), lastRefreshSha: null };
|
|
71
|
+
const gateOutcome = await runPreMergeValidationFn({
|
|
47
72
|
cwd: ctx.cwd,
|
|
48
73
|
worktreePath: ctx.worktreePath,
|
|
49
74
|
epicBranch: ctx.epicBranch,
|
|
@@ -55,6 +80,7 @@ async function runGatesAndRefresh(ctx) {
|
|
|
55
80
|
phaseTimer: ctx.phaseTimer,
|
|
56
81
|
provider: ctx.provider,
|
|
57
82
|
bus: ctx.bus,
|
|
83
|
+
cycleState,
|
|
58
84
|
});
|
|
59
85
|
if (gateOutcome?.status === 'blocked') {
|
|
60
86
|
return {
|
|
@@ -82,7 +108,7 @@ async function runGatesAndRefresh(ctx) {
|
|
|
82
108
|
}),
|
|
83
109
|
};
|
|
84
110
|
}
|
|
85
|
-
await
|
|
111
|
+
await runAutoRefreshSafelyFn(
|
|
86
112
|
{
|
|
87
113
|
storyId: ctx.storyId,
|
|
88
114
|
epicId: ctx.epicId,
|
|
@@ -90,6 +116,7 @@ async function runGatesAndRefresh(ctx) {
|
|
|
90
116
|
epicBranch: ctx.epicBranch,
|
|
91
117
|
storyBranch: ctx.storyBranch,
|
|
92
118
|
config: ctx.config,
|
|
119
|
+
cycleState,
|
|
93
120
|
},
|
|
94
121
|
{ progress: ctx.progress },
|
|
95
122
|
);
|
|
@@ -21,14 +21,8 @@
|
|
|
21
21
|
* read it; this helper writes the file and hands the path to the pipeline.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
// Legacy key on the post-merge-pipeline / cleanup-reconciler input bag.
|
|
25
|
-
// Built from substrings so the migrated-subsystem grep does not match it;
|
|
26
|
-
// the downstream helpers live outside the migrated subsystem and will
|
|
27
|
-
// rename their parameters when their own subsystems are swept.
|
|
28
|
-
const LEGACY_PIPELINE_CONFIG_KEY = `orches${'tration'}`;
|
|
29
|
-
|
|
30
24
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
31
|
-
import { emitGhSpawnCount as defaultEmitGhSpawnCount } from '../../close-validation.js';
|
|
25
|
+
import { emitGhSpawnCount as defaultEmitGhSpawnCount } from '../../close-validation/telemetry.js';
|
|
32
26
|
import { storyArtifactPath, storyTempDir } from '../../config/temp-paths.js';
|
|
33
27
|
import { gitSpawn as defaultGitSpawn } from '../../git-utils.js';
|
|
34
28
|
import { clearActiveStoryEnv as defaultClearActiveStoryEnv } from '../../observability/active-story-env.js';
|
|
@@ -340,18 +334,11 @@ export async function runPostMergeClose({
|
|
|
340
334
|
// in this order — see post-merge-pipeline.js. The `perf-summary` phase
|
|
341
335
|
// inside the pipeline shells out to analyze-execution.js, which is the
|
|
342
336
|
// single writer of the `<!-- structured:story-perf-summary -->` comment.
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
const legacyPipelineBlock = {
|
|
347
|
-
provider: 'github',
|
|
348
|
-
github: config?.github,
|
|
349
|
-
notifications: config?.github?.notifications,
|
|
350
|
-
worktreeIsolation: deliveryBlock?.worktreeIsolation,
|
|
351
|
-
runners: { deliverRunner: deliveryBlock?.deliverRunner ?? {} },
|
|
352
|
-
};
|
|
337
|
+
// The pipeline phases take the resolved `delivery` block directly, same
|
|
338
|
+
// as the cleanup-reconciler (Story #3986 — the legacy `orchestration`-keyed
|
|
339
|
+
// input bag is gone).
|
|
353
340
|
const pipelineState = await runPostMergePipeline({
|
|
354
|
-
|
|
341
|
+
delivery: deliveryBlock,
|
|
355
342
|
storyId,
|
|
356
343
|
epicId,
|
|
357
344
|
story,
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
// no-spawn spy proves the projection path never reaches a per-kind CLI
|
|
30
30
|
// subprocess.
|
|
31
31
|
import * as maintainabilityKind from '../../baselines/kinds/maintainability.js';
|
|
32
|
+
import { buildDefaultGates as defaultBuildDefaultGates } from '../../close-validation/gates.js';
|
|
32
33
|
import {
|
|
33
|
-
buildDefaultGates as defaultBuildDefaultGates,
|
|
34
34
|
formatMaintainabilityProjection as defaultFormatMaintainabilityProjection,
|
|
35
35
|
projectMaintainabilityRegressions as defaultProjectMaintainabilityRegressions,
|
|
36
|
-
|
|
37
|
-
} from '../../close-validation.js';
|
|
36
|
+
} from '../../close-validation/projections/maintainability.js';
|
|
37
|
+
import { runCloseValidation as defaultRunCloseValidation } from '../../close-validation/runner.js';
|
|
38
38
|
import { getBaselines as defaultGetBaselines } from '../../config-resolver.js';
|
|
39
39
|
import { Logger as DefaultLogger } from '../../Logger.js';
|
|
40
40
|
|
|
@@ -276,7 +276,7 @@ function detectAlreadyMerged({ cwd, storyId, epicId, lsrOut, detail, git }) {
|
|
|
276
276
|
// from the Epic history itself: locate the integration commit whose
|
|
277
277
|
// subject carries `(resolves #<id>)` / `(refs #<id>)`. Without this
|
|
278
278
|
// branch, detection falls to FRESH and the resumed close re-enters the
|
|
279
|
-
// pre-merge gate chain, which crashes in
|
|
279
|
+
// pre-merge gate chain, which crashes in the scoped format-autofix step on
|
|
280
280
|
// `git diff <epicBranch>...story-<id>` because the Story ref is gone.
|
|
281
281
|
if (!resolvedDetail) {
|
|
282
282
|
const mc = findMergeCommitForStory({ cwd, storyId, epicId, git });
|
|
@@ -431,24 +431,35 @@ export function computeRecoveryMode({ state, resume, restart } = {}) {
|
|
|
431
431
|
};
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
function dropWorktreeIfPresent({
|
|
434
|
+
function dropWorktreeIfPresent({
|
|
435
|
+
cwd,
|
|
436
|
+
wtPath,
|
|
437
|
+
progress,
|
|
438
|
+
logger,
|
|
439
|
+
gitSpawnFn = gitSpawn,
|
|
440
|
+
}) {
|
|
435
441
|
if (!fs.existsSync(wtPath)) return;
|
|
436
442
|
progress('RESTART', `Removing worktree ${wtPath}`);
|
|
437
|
-
const remove =
|
|
443
|
+
const remove = gitSpawnFn(cwd, 'worktree', 'remove', '--force', wtPath);
|
|
438
444
|
if (remove.status !== 0) {
|
|
439
445
|
logger.error(
|
|
440
446
|
`[story-close] Worktree remove failed: ${remove.stderr || 'unknown'}. ` +
|
|
441
447
|
'Attempting prune to clean stale registration.',
|
|
442
448
|
);
|
|
443
449
|
}
|
|
444
|
-
|
|
450
|
+
gitSpawnFn(cwd, 'worktree', 'prune');
|
|
445
451
|
}
|
|
446
452
|
|
|
447
|
-
function recreateStoryBranchRef({
|
|
448
|
-
|
|
449
|
-
|
|
453
|
+
function recreateStoryBranchRef({
|
|
454
|
+
cwd,
|
|
455
|
+
storyBranch,
|
|
456
|
+
epicBranch,
|
|
457
|
+
gitSpawnFn = gitSpawn,
|
|
458
|
+
}) {
|
|
459
|
+
gitSpawnFn(cwd, 'branch', '-D', storyBranch);
|
|
460
|
+
const create = gitSpawnFn(cwd, 'branch', storyBranch, epicBranch);
|
|
450
461
|
if (create.status !== 0) {
|
|
451
|
-
|
|
462
|
+
throw new Error(
|
|
452
463
|
`Failed to recreate ${storyBranch} from ${epicBranch}: ${create.stderr || 'unknown'}`,
|
|
453
464
|
);
|
|
454
465
|
}
|
|
@@ -460,13 +471,13 @@ function reseedWorktreeIfNeeded({
|
|
|
460
471
|
storyId,
|
|
461
472
|
storyBranch,
|
|
462
473
|
progress,
|
|
463
|
-
|
|
474
|
+
gitSpawnFn = gitSpawn,
|
|
464
475
|
}) {
|
|
465
476
|
if (!wtConfig?.enabled) return;
|
|
466
477
|
const wtPath = storyWorktreePath(cwd, storyId, wtConfig.root);
|
|
467
|
-
const add =
|
|
478
|
+
const add = gitSpawnFn(cwd, 'worktree', 'add', wtPath, storyBranch);
|
|
468
479
|
if (add.status !== 0) {
|
|
469
|
-
|
|
480
|
+
throw new Error(
|
|
470
481
|
`Failed to re-seed worktree at ${wtPath}: ${add.stderr || 'unknown'}`,
|
|
471
482
|
);
|
|
472
483
|
}
|
|
@@ -477,8 +488,12 @@ function reseedWorktreeIfNeeded({
|
|
|
477
488
|
* Restart path: abort any in-progress merge, drop the worktree, delete the
|
|
478
489
|
* story branch ref, and re-seed branch + worktree from the Epic branch. The
|
|
479
490
|
* caller then falls through to the normal fresh-close flow.
|
|
491
|
+
*
|
|
492
|
+
* Throws (never `logger.fatal`) on a failed branch recreate or worktree
|
|
493
|
+
* re-seed, per `rules/orchestration-error-handling.md` — a failed recreate
|
|
494
|
+
* MUST NOT fall through into the worktree re-seed.
|
|
480
495
|
*/
|
|
481
|
-
function restartStoryState({
|
|
496
|
+
export function restartStoryState({
|
|
482
497
|
cwd,
|
|
483
498
|
orchestration,
|
|
484
499
|
storyId,
|
|
@@ -486,9 +501,10 @@ function restartStoryState({
|
|
|
486
501
|
storyBranch,
|
|
487
502
|
progress = () => {},
|
|
488
503
|
logger = Logger,
|
|
504
|
+
gitSpawnFn = gitSpawn,
|
|
489
505
|
} = {}) {
|
|
490
506
|
progress('RESTART', `Resetting prior state for Story #${storyId}...`);
|
|
491
|
-
|
|
507
|
+
gitSpawnFn(cwd, 'merge', '--abort');
|
|
492
508
|
|
|
493
509
|
const wtConfig = orchestration?.worktreeIsolation;
|
|
494
510
|
if (wtConfig?.enabled) {
|
|
@@ -497,17 +513,18 @@ function restartStoryState({
|
|
|
497
513
|
wtPath: storyWorktreePath(cwd, storyId, wtConfig.root),
|
|
498
514
|
progress,
|
|
499
515
|
logger,
|
|
516
|
+
gitSpawnFn,
|
|
500
517
|
});
|
|
501
518
|
}
|
|
502
519
|
|
|
503
|
-
recreateStoryBranchRef({ cwd, storyBranch, epicBranch,
|
|
520
|
+
recreateStoryBranchRef({ cwd, storyBranch, epicBranch, gitSpawnFn });
|
|
504
521
|
reseedWorktreeIfNeeded({
|
|
505
522
|
cwd,
|
|
506
523
|
wtConfig,
|
|
507
524
|
storyId,
|
|
508
525
|
storyBranch,
|
|
509
526
|
progress,
|
|
510
|
-
|
|
527
|
+
gitSpawnFn,
|
|
511
528
|
});
|
|
512
529
|
}
|
|
513
530
|
|
|
@@ -563,7 +580,7 @@ export function dispatchRecovery({
|
|
|
563
580
|
restartFn = restartStoryState,
|
|
564
581
|
} = {}) {
|
|
565
582
|
if (resume && restart) {
|
|
566
|
-
|
|
583
|
+
throw new Error('--resume and --restart are mutually exclusive');
|
|
567
584
|
}
|
|
568
585
|
|
|
569
586
|
const priorPhase = detectFn({ cwd, storyId, epicId });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/story-reachability.js — Story `depends_on` graph walk.
|
|
3
|
+
*
|
|
4
|
+
* Houses the transitive-predecessor closure over the story-level
|
|
5
|
+
* `depends_on` graph. Extracted from `./ticket-validator-conflicts.js`
|
|
6
|
+
* under Story #3995 to break the `file-assumptions.js ↔
|
|
7
|
+
* ticket-validator-conflicts.js` import cycle: both the conflict gate and
|
|
8
|
+
* the wave-aware file-assumption gate need this graph traversal, so
|
|
9
|
+
* pulling it down into a dependency-free leaf lets both import it from
|
|
10
|
+
* below rather than from each other.
|
|
11
|
+
*
|
|
12
|
+
* This is a pure graph utility — no I/O, no policy. Its behaviour is
|
|
13
|
+
* unchanged from the original `computeStoryReachability`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compute transitive predecessor sets over the story-level `depends_on`
|
|
18
|
+
* graph. The returned map is `Map<storySlug, Set<storySlug>>`, where the
|
|
19
|
+
* set contains every story reachable by following `depends_on` edges from
|
|
20
|
+
* the key (i.e. every story the key transitively depends on).
|
|
21
|
+
*
|
|
22
|
+
* BFS, no cycles assumed — callers must run `assertAcyclic` first.
|
|
23
|
+
*
|
|
24
|
+
* Exported so both the conflict gate (`ticket-validator-conflicts.js`)
|
|
25
|
+
* and the wave-aware file-assumption gate (`file-assumptions.js`) can
|
|
26
|
+
* reuse the same transitive-predecessor walk rather than re-deriving the
|
|
27
|
+
* `depends_on` closure.
|
|
28
|
+
*/
|
|
29
|
+
export function computeStoryReachability(stories) {
|
|
30
|
+
const reach = new Map();
|
|
31
|
+
for (const story of stories) reach.set(story.slug, new Set());
|
|
32
|
+
for (const story of stories) {
|
|
33
|
+
const visited = reach.get(story.slug);
|
|
34
|
+
const stack = [...(story.depends_on ?? [])];
|
|
35
|
+
while (stack.length > 0) {
|
|
36
|
+
const next = stack.pop();
|
|
37
|
+
if (!reach.has(next)) continue;
|
|
38
|
+
if (visited.has(next)) continue;
|
|
39
|
+
visited.add(next);
|
|
40
|
+
const nextStory = stories.find((s) => s.slug === next);
|
|
41
|
+
if (nextStory && Array.isArray(nextStory.depends_on)) {
|
|
42
|
+
for (const dep of nextStory.depends_on) stack.push(dep);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return reach;
|
|
47
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectStoryAssumptionEntries } from './file-assumptions.js';
|
|
2
|
+
import { computeStoryReachability } from './story-reachability.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Cross-Story path-conflict & implicit-dependency findings.
|
|
@@ -66,7 +67,7 @@ const DEFAULT_POLICY = Object.freeze({
|
|
|
66
67
|
* - exact path — `lib/orchestration/lifecycle/listeners/index.js`
|
|
67
68
|
* - `**` suffix — `**\/listeners/index.js` (matches any depth)
|
|
68
69
|
*/
|
|
69
|
-
const DEFAULT_REGISTRY_PATTERNS = Object.freeze([
|
|
70
|
+
export const DEFAULT_REGISTRY_PATTERNS = Object.freeze([
|
|
70
71
|
'lib/orchestration/lifecycle/listeners/index.js',
|
|
71
72
|
'**/listeners/index.js',
|
|
72
73
|
'**/handlers/index.js',
|
|
@@ -242,38 +243,6 @@ function indexConsumers(stories, producers) {
|
|
|
242
243
|
return consumers;
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
/**
|
|
246
|
-
* Compute transitive predecessor sets over the story-level `depends_on`
|
|
247
|
-
* graph. The returned map is `Map<storySlug, Set<storySlug>>`, where the
|
|
248
|
-
* set contains every story reachable by following `depends_on` edges from
|
|
249
|
-
* the key (i.e. every story the key transitively depends on).
|
|
250
|
-
*
|
|
251
|
-
* BFS, no cycles assumed — callers must run `assertAcyclic` first.
|
|
252
|
-
*
|
|
253
|
-
* Exported so the wave-aware file-assumption gate
|
|
254
|
-
* (`file-assumptions.js`) can reuse the same transitive-predecessor walk
|
|
255
|
-
* rather than re-deriving the `depends_on` closure.
|
|
256
|
-
*/
|
|
257
|
-
export function computeStoryReachability(stories) {
|
|
258
|
-
const reach = new Map();
|
|
259
|
-
for (const story of stories) reach.set(story.slug, new Set());
|
|
260
|
-
for (const story of stories) {
|
|
261
|
-
const visited = reach.get(story.slug);
|
|
262
|
-
const stack = [...(story.depends_on ?? [])];
|
|
263
|
-
while (stack.length > 0) {
|
|
264
|
-
const next = stack.pop();
|
|
265
|
-
if (!reach.has(next)) continue;
|
|
266
|
-
if (visited.has(next)) continue;
|
|
267
|
-
visited.add(next);
|
|
268
|
-
const nextStory = stories.find((s) => s.slug === next);
|
|
269
|
-
if (nextStory && Array.isArray(nextStory.depends_on)) {
|
|
270
|
-
for (const dep of nextStory.depends_on) stack.push(dep);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return reach;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
246
|
function inSameWave(reach, slugA, slugB) {
|
|
278
247
|
if (slugA === slugB) return false;
|
|
279
248
|
const a = reach.get(slugA);
|
|
@@ -8,26 +8,25 @@
|
|
|
8
8
|
* Story #1848 so the verb-family split (`reads` / `state` / `bulk`) is
|
|
9
9
|
* complete and the parent collapses to a pure re-export facade.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Story #3995 — the single-ticket mutators (`transitionTicketState`,
|
|
12
|
+
* `toggleTasklistCheckbox`, `postStructuredComment`) moved to the leaf
|
|
13
|
+
* `./transition.js`, so this module now depends **downward** on
|
|
14
|
+
* `transition.js` and the former `state.js ↔ bulk.js` import cycle is
|
|
15
|
+
* gone. `cascadeCompletion` still recursively transitions parents via
|
|
16
|
+
* the imported `transitionTicketState`; the reverse call
|
|
17
|
+
* (`transitionTicketState → cascadeParentState`) is injected into
|
|
18
|
+
* `transition.js` by `state.js` via `registerCascadeRunner` rather than
|
|
19
|
+
* imported, which is what keeps the graph acyclic.
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { Logger } from '../../Logger.js';
|
|
19
23
|
import { TYPE_LABELS } from '../../label-constants.js';
|
|
20
|
-
import { dispatchCascadeGroups, groupByAncestor } from '../cascade-grouping.js';
|
|
21
24
|
import { ALL_STATES, STATE_LABELS } from './reads.js';
|
|
22
25
|
import {
|
|
23
26
|
postStructuredComment,
|
|
24
27
|
toggleTasklistCheckbox,
|
|
25
28
|
transitionTicketState,
|
|
26
|
-
} from './
|
|
27
|
-
|
|
28
|
-
// Re-export `groupByAncestor` so external callers that imported it from
|
|
29
|
-
// the ticketing facade continue to work after the verb-family split.
|
|
30
|
-
export { groupByAncestor };
|
|
29
|
+
} from './transition.js';
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* Retry budget for transient `gh` failures (rate limit, secondary rate limit,
|
|
@@ -217,9 +216,8 @@ export function logCascadePartialFailures(ticketId, cascade) {
|
|
|
217
216
|
|
|
218
217
|
/**
|
|
219
218
|
* Per-parent body of {@link cascadeCompletion}. Pulled out so the outer
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* log output across serial and parallel execution paths.
|
|
219
|
+
* walk stays a thin sequential loop while the per-parent work runs under
|
|
220
|
+
* the per-parent cascade lock.
|
|
223
221
|
*
|
|
224
222
|
* @param {import('../../ITicketingProvider.js').ITicketingProvider} provider
|
|
225
223
|
* @param {number} ticketId - The ticket whose `agent::done` transition
|
|
@@ -372,19 +370,13 @@ async function processCascadeParentLocked(
|
|
|
372
370
|
* Then checks if parent's sub-tickets are ALL DONE.
|
|
373
371
|
* If yes, transitions parent to DONE and cascades up.
|
|
374
372
|
*
|
|
375
|
-
* Parents
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* Log output is captured per parent into a buffered logger and flushed
|
|
384
|
-
* to the real {@link Logger} after all groups resolve, in the original
|
|
385
|
-
* `parsedParents` order. The visible log stream is therefore
|
|
386
|
-
* byte-identical to a serial baseline; only the I/O between parents in
|
|
387
|
-
* disjoint groups overlaps.
|
|
373
|
+
* Parents run strictly sequentially in input order (Story #4017 —
|
|
374
|
+
* fan-out is <= 1 under the 3-tier hierarchy, so the former
|
|
375
|
+
* shared-ancestor grouping / parallel dispatch was deleted); concurrent
|
|
376
|
+
* transitions against a shared ancestor would race the "all children
|
|
377
|
+
* done?" check. Within each parent, sibling reads fan out via
|
|
378
|
+
* `getSubTickets(parentId, { fresh: true })` with the concurrency cap
|
|
379
|
+
* (8) applied inside `getSubTickets`.
|
|
388
380
|
*
|
|
389
381
|
* Per-parent errors are isolated: a failure updating one parent (network,
|
|
390
382
|
* permission, stale ticket) never discards progress on sibling parents.
|
|
@@ -450,26 +442,19 @@ export async function cascadeCompletion(provider, ticketId, opts = {}) {
|
|
|
450
442
|
return { cascadedTo: [], failed: [] };
|
|
451
443
|
}
|
|
452
444
|
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
const results = await dispatchCascadeGroups({
|
|
460
|
-
parsedParents,
|
|
461
|
-
groups,
|
|
462
|
-
flushLogger: opts._logger ?? Logger,
|
|
463
|
-
processParent: (parentId, logger) =>
|
|
464
|
-
processCascadeParent(provider, ticketId, parentId, {
|
|
465
|
-
notify: opts.notify,
|
|
466
|
-
_logger: logger,
|
|
467
|
-
}),
|
|
468
|
-
});
|
|
469
|
-
|
|
445
|
+
// Story #4017 — under the 3-tier hierarchy a ticket has at most one
|
|
446
|
+
// parent, so the shared-ancestor grouping / parallel-group dispatch
|
|
447
|
+
// machinery was deleted. Parents (fan-out <= 1
|
|
448
|
+
// in practice; the loop stays general for the body-reference fallback)
|
|
449
|
+
// run strictly sequentially, which trivially preserves the
|
|
450
|
+
// shared-ancestor safety invariant the grouping used to enforce.
|
|
470
451
|
const cascadedTo = [];
|
|
471
452
|
const failed = [];
|
|
472
|
-
for (const
|
|
453
|
+
for (const parentId of parsedParents) {
|
|
454
|
+
const r = await processCascadeParent(provider, ticketId, parentId, {
|
|
455
|
+
notify: opts.notify,
|
|
456
|
+
_logger: opts._logger,
|
|
457
|
+
});
|
|
473
458
|
cascadedTo.push(...r.cascadedTo);
|
|
474
459
|
failed.push(...r.failed);
|
|
475
460
|
}
|
|
@@ -530,12 +515,11 @@ export function deriveParentState(siblings) {
|
|
|
530
515
|
* recursive cascade" progress comment, Epic exclusion) are preserved
|
|
531
516
|
* verbatim.
|
|
532
517
|
*
|
|
533
|
-
* Resilience matches {@link cascadeCompletion}:
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* discard work on the others.
|
|
518
|
+
* Resilience matches {@link cascadeCompletion}: parents run
|
|
519
|
+
* sequentially; the per-parent lock from {@link withParentCascadeLock}
|
|
520
|
+
* prevents races on shared ancestors; and per-parent errors are
|
|
521
|
+
* isolated so a sibling parent's failure does not discard work on the
|
|
522
|
+
* others.
|
|
539
523
|
*
|
|
540
524
|
* @param {import('../../ITicketingProvider.js').ITicketingProvider} provider
|
|
541
525
|
* @param {number} ticketId
|
|
@@ -572,21 +556,15 @@ export async function cascadeParentState(provider, ticketId, opts = {}) {
|
|
|
572
556
|
const parsedParents = await resolveParentIds(provider, ticket, ticketId);
|
|
573
557
|
if (parsedParents.length === 0) return { cascadedTo: [], failed: [] };
|
|
574
558
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
parsedParents,
|
|
578
|
-
groups,
|
|
579
|
-
flushLogger: opts._logger ?? Logger,
|
|
580
|
-
processParent: (parentId, logger) =>
|
|
581
|
-
processStateCascadeParent(provider, parentId, {
|
|
582
|
-
notify: opts.notify,
|
|
583
|
-
_logger: logger,
|
|
584
|
-
}),
|
|
585
|
-
});
|
|
586
|
-
|
|
559
|
+
// Story #4017 — sequential per-parent walk (fan-out <= 1 under the
|
|
560
|
+
// 3-tier hierarchy); see cascadeCompletion for the rationale.
|
|
587
561
|
const cascadedTo = [];
|
|
588
562
|
const failed = [];
|
|
589
|
-
for (const
|
|
563
|
+
for (const parentId of parsedParents) {
|
|
564
|
+
const r = await processStateCascadeParent(provider, parentId, {
|
|
565
|
+
notify: opts.notify,
|
|
566
|
+
_logger: opts._logger,
|
|
567
|
+
});
|
|
590
568
|
cascadedTo.push(...r.cascadedTo);
|
|
591
569
|
failed.push(...r.failed);
|
|
592
570
|
}
|
|
@@ -151,6 +151,15 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
151
151
|
// from it (`deriveRiskEnvelope`). One entry per Epic; re-plans upsert in
|
|
152
152
|
// place. Schema: `.agents/schemas/risk-verdict.schema.json`.
|
|
153
153
|
'risk-verdict',
|
|
154
|
+
// Story #4019 — `epic-plan-lease-guard.js` upserts a `plan-lease`
|
|
155
|
+
// comment on the Epic at lease-acquire time, recording the claiming
|
|
156
|
+
// operator and the claim timestamp. `/epic-plan` emits no
|
|
157
|
+
// `story.heartbeat`, so this claim-time is the liveness signal that
|
|
158
|
+
// makes the documented `--steal` contract decidable: a foreign claim
|
|
159
|
+
// older than the lease TTL is reclaimed automatically; a fresh one
|
|
160
|
+
// refuses with the claim age. One entry per Epic; re-acquires upsert
|
|
161
|
+
// in place.
|
|
162
|
+
'plan-lease',
|
|
154
163
|
]);
|
|
155
164
|
|
|
156
165
|
export const WAVE_TYPE_PATTERN = WAVE_MARKER_RE;
|