mandrel 2.30.0 → 2.32.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 +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/ci-remediation.md +44 -1
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-entry-sync.js +469 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/story-body.js +12 -59
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +0 -2
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +49 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +12 -3
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Span-tree builder (Epic #1181 / Story #1440 / Task #1461).
|
|
3
|
-
*
|
|
4
|
-
* Pure transform over the `lib/signals/read` async iterator. Materialises
|
|
5
|
-
* a tree of:
|
|
6
|
-
*
|
|
7
|
-
* epic
|
|
8
|
-
* story (id)
|
|
9
|
-
* task (id)
|
|
10
|
-
* events (chronological)
|
|
11
|
-
*
|
|
12
|
-
* with `startedAt` / `endedAt` / `durationMs` computed from paired
|
|
13
|
-
* lifecycle events when they exist. The Tech Spec (#1433) defines the
|
|
14
|
-
* shape; this module is the canonical implementation.
|
|
15
|
-
*
|
|
16
|
-
* ## Lifecycle pairing
|
|
17
|
-
*
|
|
18
|
-
* The schema audit in `lib/signals/schema.js` confirms that
|
|
19
|
-
* `wave-start` / `wave-end` envelopes carry `epicId` + `storyId` and
|
|
20
|
-
* bracket a Story's lifetime in a wave.
|
|
21
|
-
* Per-Task lifecycle today comes via `state-transition` events
|
|
22
|
-
* (`agent::executing` → `agent::done`) — we treat the **first**
|
|
23
|
-
* timestamp we see for a given (story, task) pair as `startedAt` and
|
|
24
|
-
* the **last** as `endedAt`. This is intentionally permissive: the
|
|
25
|
-
* span-tree's contract is "what we have", not "what we wish were
|
|
26
|
-
* emitted". When a Task has only a start event the `durationMs` stays
|
|
27
|
-
* `null` per AC#2.
|
|
28
|
-
*
|
|
29
|
-
* `wave-start` / `wave-end` (or any kind starting with `story.`)
|
|
30
|
-
* anchor the Story-level start/end when present; absent those, we
|
|
31
|
-
* fall back to the min/max of every event under that Story.
|
|
32
|
-
*
|
|
33
|
-
* ## Purity
|
|
34
|
-
*
|
|
35
|
-
* No I/O, no globals, no `Date.now()` reads. The function is a pure
|
|
36
|
-
* reducer over the iterator — identical input produces identical
|
|
37
|
-
* output (AC#1). Time references come exclusively from the events'
|
|
38
|
-
* own `ts` / `timestamp` fields.
|
|
39
|
-
*
|
|
40
|
-
* ## Empty input
|
|
41
|
-
*
|
|
42
|
-
* `buildSpanTree(emptyIter)` with **no events** returns
|
|
43
|
-
* `{ epic: null, stories: [] }` — the Epic is unknown when we never
|
|
44
|
-
* see one (AC#3). When at least one event flows through we pin
|
|
45
|
-
* `epic` to its `epic` / `epicId` field; mixed-Epic iterators
|
|
46
|
-
* (uncommon — `read()` is single-Epic) take the first observed Epic
|
|
47
|
-
* ID and ignore the rest with no warning, since silent permissiveness
|
|
48
|
-
* is the pure-function contract.
|
|
49
|
-
*
|
|
50
|
-
* @module lib/signals/span-tree
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
function tsOf(evt) {
|
|
54
|
-
// Canonical envelope only (Epic #4406) — the legacy `timestamp` alias
|
|
55
|
-
// was deleted from every writer in the same PR.
|
|
56
|
-
return evt?.ts ?? null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function epicOf(evt) {
|
|
60
|
-
return evt?.epicId ?? null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function storyOf(evt) {
|
|
64
|
-
return evt?.storyId ?? null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function taskOf(evt) {
|
|
68
|
-
return evt?.taskId ?? null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Lexicographic timestamp comparison. ISO-8601 strings compare correctly
|
|
73
|
-
* with `<` / `>`, so we avoid the Date round-trip and stay pure (no TZ
|
|
74
|
-
* normalisation surprises).
|
|
75
|
-
*
|
|
76
|
-
* @param {string | null} a
|
|
77
|
-
* @param {string | null} b
|
|
78
|
-
* @returns {number} -1 / 0 / 1 (nulls sort last)
|
|
79
|
-
*/
|
|
80
|
-
function cmpTs(a, b) {
|
|
81
|
-
if (a === b) return 0;
|
|
82
|
-
if (a == null) return 1;
|
|
83
|
-
if (b == null) return -1;
|
|
84
|
-
if (a < b) return -1;
|
|
85
|
-
if (a > b) return 1;
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function diffMs(start, end) {
|
|
90
|
-
if (start == null || end == null) return null;
|
|
91
|
-
const s = Date.parse(start);
|
|
92
|
-
const e = Date.parse(end);
|
|
93
|
-
if (!Number.isFinite(s) || !Number.isFinite(e)) return null;
|
|
94
|
-
return e - s;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isStoryLifecycle(kind) {
|
|
98
|
-
return (
|
|
99
|
-
kind === 'wave-start' ||
|
|
100
|
-
kind === 'wave-end' ||
|
|
101
|
-
kind === 'story.start' ||
|
|
102
|
-
kind === 'story.end'
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isStoryStart(kind) {
|
|
107
|
-
return kind === 'wave-start' || kind === 'story.start';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function isStoryEnd(kind) {
|
|
111
|
-
return kind === 'wave-end' || kind === 'story.end';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function emptyTaskNode(id) {
|
|
115
|
-
return {
|
|
116
|
-
id,
|
|
117
|
-
startedAt: null,
|
|
118
|
-
endedAt: null,
|
|
119
|
-
durationMs: null,
|
|
120
|
-
events: [],
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function emptyStoryNode(id) {
|
|
125
|
-
return {
|
|
126
|
-
id,
|
|
127
|
-
startedAt: null,
|
|
128
|
-
endedAt: null,
|
|
129
|
-
durationMs: null,
|
|
130
|
-
tasks: [],
|
|
131
|
-
events: [],
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function storyKeyOf(sid) {
|
|
136
|
-
return sid == null ? '__none__' : String(sid);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function getOrCreateStory(state, sid) {
|
|
140
|
-
const key = storyKeyOf(sid);
|
|
141
|
-
let node = state.stories.get(key);
|
|
142
|
-
if (!node) {
|
|
143
|
-
node = emptyStoryNode(sid);
|
|
144
|
-
state.stories.set(key, node);
|
|
145
|
-
state.tasksByStory.set(key, new Map());
|
|
146
|
-
}
|
|
147
|
-
return { key, node };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getOrCreateTask(state, storyKey, tid) {
|
|
151
|
-
const taskMap = state.tasksByStory.get(storyKey);
|
|
152
|
-
const tkey = String(tid);
|
|
153
|
-
let node = taskMap.get(tkey);
|
|
154
|
-
if (!node) {
|
|
155
|
-
node = emptyTaskNode(tid);
|
|
156
|
-
taskMap.set(tkey, node);
|
|
157
|
-
}
|
|
158
|
-
return node;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function updateStoryLifecycle(storyNode, kind, ts) {
|
|
162
|
-
if (!isStoryLifecycle(kind)) return;
|
|
163
|
-
if (isStoryStart(kind)) {
|
|
164
|
-
if (storyNode.startedAt == null || cmpTs(ts, storyNode.startedAt) < 0) {
|
|
165
|
-
storyNode.startedAt = ts;
|
|
166
|
-
}
|
|
167
|
-
} else if (isStoryEnd(kind)) {
|
|
168
|
-
if (storyNode.endedAt == null || cmpTs(ts, storyNode.endedAt) > 0) {
|
|
169
|
-
storyNode.endedAt = ts;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function widenTaskWindow(taskNode, ts) {
|
|
175
|
-
if (ts == null) return;
|
|
176
|
-
if (taskNode.startedAt == null || cmpTs(ts, taskNode.startedAt) < 0) {
|
|
177
|
-
taskNode.startedAt = ts;
|
|
178
|
-
}
|
|
179
|
-
if (taskNode.endedAt == null || cmpTs(ts, taskNode.endedAt) > 0) {
|
|
180
|
-
taskNode.endedAt = ts;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function bootstrapStoryWindow(storyNode, ts) {
|
|
185
|
-
if (ts == null) return;
|
|
186
|
-
if (storyNode.startedAt == null) storyNode.startedAt = ts;
|
|
187
|
-
if (storyNode.endedAt == null) storyNode.endedAt = ts;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Reducer step — fold a single event into the running state. Extracted
|
|
192
|
-
* from `buildSpanTree` so the outer function's CRAP score stays under
|
|
193
|
-
* the new-method ceiling.
|
|
194
|
-
*
|
|
195
|
-
* @param {object} state — mutable accumulator (stories, tasksByStory, run)
|
|
196
|
-
* @param {unknown} evt
|
|
197
|
-
* @returns {void}
|
|
198
|
-
*/
|
|
199
|
-
function ingestEvent(state, evt) {
|
|
200
|
-
if (evt == null || typeof evt !== 'object') return;
|
|
201
|
-
|
|
202
|
-
const eRun = epicOf(evt);
|
|
203
|
-
if (state.run == null && eRun != null) state.run = eRun;
|
|
204
|
-
|
|
205
|
-
const ts = tsOf(evt);
|
|
206
|
-
const sid = storyOf(evt);
|
|
207
|
-
const tid = taskOf(evt);
|
|
208
|
-
const kind = typeof evt.kind === 'string' ? evt.kind : null;
|
|
209
|
-
|
|
210
|
-
const { key: storyKey, node: storyNode } = getOrCreateStory(state, sid);
|
|
211
|
-
updateStoryLifecycle(storyNode, kind, ts);
|
|
212
|
-
|
|
213
|
-
if (tid != null) {
|
|
214
|
-
const taskNode = getOrCreateTask(state, storyKey, tid);
|
|
215
|
-
widenTaskWindow(taskNode, ts);
|
|
216
|
-
taskNode.events.push(evt);
|
|
217
|
-
} else {
|
|
218
|
-
storyNode.events.push(evt);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
bootstrapStoryWindow(storyNode, ts);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Default comparator for sorting Story / Task nodes by id ascending,
|
|
226
|
-
* with `null` ids sorting last and a string fallback for slug-shaped
|
|
227
|
-
* Task ids (the framework allows non-numeric task identifiers).
|
|
228
|
-
*
|
|
229
|
-
* @param {object} a
|
|
230
|
-
* @param {object} b
|
|
231
|
-
* @returns {number}
|
|
232
|
-
*/
|
|
233
|
-
function compareNodeIds(a, b) {
|
|
234
|
-
if (a.id === b.id) return 0;
|
|
235
|
-
if (a.id == null) return 1;
|
|
236
|
-
if (b.id == null) return -1;
|
|
237
|
-
if (typeof a.id === 'number' && typeof b.id === 'number') {
|
|
238
|
-
return a.id - b.id;
|
|
239
|
-
}
|
|
240
|
-
return String(a.id).localeCompare(String(b.id));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function finalizeTask(task) {
|
|
244
|
-
task.events.sort((ea, eb) => cmpTs(tsOf(ea), tsOf(eb)));
|
|
245
|
-
task.durationMs = diffMs(task.startedAt, task.endedAt);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function finalizeStory(state, story) {
|
|
249
|
-
const taskMap = state.tasksByStory.get(storyKeyOf(story.id));
|
|
250
|
-
const taskEntries = [...taskMap.values()];
|
|
251
|
-
taskEntries.sort(compareNodeIds);
|
|
252
|
-
for (const task of taskEntries) finalizeTask(task);
|
|
253
|
-
story.events.sort((ea, eb) => cmpTs(tsOf(ea), tsOf(eb)));
|
|
254
|
-
story.durationMs = diffMs(story.startedAt, story.endedAt);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Build the span tree from an async iterable of signal events.
|
|
259
|
-
*
|
|
260
|
-
* @param {AsyncIterable<object> | Iterable<object>} iter
|
|
261
|
-
* @returns {Promise<{ run: number | null, stories: Array<object> }>}
|
|
262
|
-
*
|
|
263
|
-
* @example
|
|
264
|
-
* import { read, buildSpanTree } from './lib/signals/index.js';
|
|
265
|
-
* const tree = await buildSpanTree(read({ run: 1181 }));
|
|
266
|
-
*/
|
|
267
|
-
export async function buildSpanTree(iter) {
|
|
268
|
-
if (iter == null || typeof iter !== 'object') {
|
|
269
|
-
throw new TypeError(
|
|
270
|
-
`signals/span-tree: iter must be an async iterable; got ${iter}`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Accumulator state. `stories` keys are `storyKeyOf(sid)` strings;
|
|
275
|
-
// `tasksByStory` mirrors that and holds per-task sub-maps.
|
|
276
|
-
const state = {
|
|
277
|
-
run: null,
|
|
278
|
-
stories: new Map(),
|
|
279
|
-
tasksByStory: new Map(),
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
for await (const evt of iter) {
|
|
283
|
-
ingestEvent(state, evt);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const storyEntries = [...state.stories.values()];
|
|
287
|
-
storyEntries.sort(compareNodeIds);
|
|
288
|
-
for (const story of storyEntries) finalizeStory(state, story);
|
|
289
|
-
|
|
290
|
-
return { run: state.run, stories: storyEntries };
|
|
291
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Story lifecycle helpers shared between story-init and story-close.
|
|
3
|
-
*
|
|
4
|
-
* These three pure/IO-helpers capture the overlap previously duplicated across
|
|
5
|
-
* the two CLI scripts — parsing the `Epic: #N` / `parent: #N` references out
|
|
6
|
-
* of a Story body, fetching child sub-tickets, and batch-transitioning
|
|
7
|
-
* tickets to a target state label.
|
|
8
|
-
*
|
|
9
|
-
* The shape is narrow on purpose: init/close still own their own orchestration
|
|
10
|
-
* (branch bootstrap, merge, cascade, notifications). Expanding this module to
|
|
11
|
-
* cover those would over-abstract — they are genuinely different concerns.
|
|
12
|
-
*
|
|
13
|
-
* Under the 2-tier hierarchy (Epic #3078) Stories have no child tickets, so
|
|
14
|
-
* `fetchChildTickets` typically returns `[]` — but the helper remains in
|
|
15
|
-
* place so legacy callers still resolve the Story's direct children cleanly.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { Logger } from './Logger.js';
|
|
19
|
-
import {
|
|
20
|
-
STATE_LABELS,
|
|
21
|
-
transitionTicketState,
|
|
22
|
-
} from './orchestration/ticketing.js';
|
|
23
|
-
/**
|
|
24
|
-
* Parse the `Epic: #N` and `parent: #N` references from a Story body.
|
|
25
|
-
*
|
|
26
|
-
* Story #4300 (defense-in-depth): under the 2-tier hierarchy
|
|
27
|
-
* (Epic → Story) a Story's `parent: #N` marker always IS the parent
|
|
28
|
-
* Epic, so when the `Epic: #N` line is missing — e.g. a Story body
|
|
29
|
-
* refreshed by the reconciler's UPDATE op before Story #4300's
|
|
30
|
-
* single-sourced footer rendering landed — `epicId` falls back to the
|
|
31
|
-
* resolved `parentId` rather than reporting `null` and aborting
|
|
32
|
-
* delivery. A body that carries neither marker still resolves
|
|
33
|
-
* `epicId: null` (no parent to fall back to).
|
|
34
|
-
*
|
|
35
|
-
* @param {string} body Raw Story body Markdown.
|
|
36
|
-
* @returns {{ epicId: number|null, parentId: number|null }}
|
|
37
|
-
*/
|
|
38
|
-
export function resolveStoryHierarchy(body) {
|
|
39
|
-
const source = body ?? '';
|
|
40
|
-
const epicMatch = source.match(/(?:^\s*epic:\s*#(\d+))/im);
|
|
41
|
-
const parentMatch = source.match(/(?:^\s*parent:\s*#(\d+))/im);
|
|
42
|
-
const parentId = parentMatch ? Number.parseInt(parentMatch[1], 10) : null;
|
|
43
|
-
const epicId = epicMatch ? Number.parseInt(epicMatch[1], 10) : parentId;
|
|
44
|
-
return { epicId, parentId };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Fetch the Story's direct child tickets via the provider.
|
|
49
|
-
*
|
|
50
|
-
* Under the 2-tier hierarchy (Epic #3078) Stories no longer enumerate
|
|
51
|
-
* child Task tickets — acceptance criteria and verification steps live
|
|
52
|
-
* inline on the Story body, and decomposers emit only Epic / Story
|
|
53
|
-
* issues. For those Stories this helper resolves to an empty
|
|
54
|
-
* array because `provider.getSubTickets(storyId)` itself returns no
|
|
55
|
-
* rows. The helper is retained as a thin pass-through so the three
|
|
56
|
-
* orchestration callers (`story-init` prepare, `task-graph-builder`,
|
|
57
|
-
* and `locked-pipeline`) keep a single, named seam for sub-ticket
|
|
58
|
-
* hydration that is easy to mock in tests and to instrument in the
|
|
59
|
-
* provider layer.
|
|
60
|
-
*
|
|
61
|
-
* Legacy ticket trees that were planned before the 2-tier cutover may
|
|
62
|
-
* still carry sub-tickets (PRD/Tech-Spec links recorded as direct
|
|
63
|
-
* children, for example). The helper deliberately does **not** filter
|
|
64
|
-
* those out — the caller is responsible for classifying anything that
|
|
65
|
-
* comes back. The previous `type::task`-only filter has been removed in
|
|
66
|
-
* Story #3191 because no current call site relies on that narrowing.
|
|
67
|
-
*
|
|
68
|
-
* @param {object} provider ITicketingProvider instance.
|
|
69
|
-
* @param {number} storyId Story ticket number to hydrate children for.
|
|
70
|
-
* @returns {Promise<object[]>} Array of child tickets (empty under 2-tier).
|
|
71
|
-
*/
|
|
72
|
-
export async function fetchChildTickets(provider, storyId) {
|
|
73
|
-
return provider.getSubTickets(storyId);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Detect transient HTTP errors (rate limits, gateways, server errors) that
|
|
78
|
-
* are worth retrying. Permission/validation errors are not retried — the
|
|
79
|
-
* second attempt would fail the same way.
|
|
80
|
-
*
|
|
81
|
-
* @param {Error} err
|
|
82
|
-
* @returns {boolean}
|
|
83
|
-
*/
|
|
84
|
-
function isRetryableError(err) {
|
|
85
|
-
const status = err?.status ?? err?.statusCode;
|
|
86
|
-
if (status === 429) return true;
|
|
87
|
-
if (typeof status === 'number' && status >= 500 && status < 600) return true;
|
|
88
|
-
const msg = (err?.message ?? '').toLowerCase();
|
|
89
|
-
return (
|
|
90
|
-
msg.includes('rate limit') ||
|
|
91
|
-
msg.includes('timeout') ||
|
|
92
|
-
msg.includes('econnreset') ||
|
|
93
|
-
msg.includes('etimedout') ||
|
|
94
|
-
msg.includes('socket hang up')
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Batch-transition a set of tickets to the target state label. Tickets
|
|
100
|
-
* already carrying `STATE_LABELS.DONE` (or the target itself) are skipped.
|
|
101
|
-
* Each transition runs in parallel with a per-ticket retry budget for
|
|
102
|
-
* transient errors (429, 5xx, network). Permanent failures are recorded
|
|
103
|
-
* in `failed` without aborting the batch.
|
|
104
|
-
*
|
|
105
|
-
* @param {object} provider ITicketingProvider instance.
|
|
106
|
-
* @param {object[]} tickets Array of ticket objects with `id` and `labels`.
|
|
107
|
-
* @param {string} targetLabel Target `agent::...` label (e.g. STATE_LABELS.EXECUTING).
|
|
108
|
-
* @param {object} [opts]
|
|
109
|
-
* @param {(phase: string, message: string) => void} [opts.progress] Progress reporter.
|
|
110
|
-
* @param {(ticketId: number, err: Error) => void} [opts.onError] Per-failure callback.
|
|
111
|
-
* @param {number} [opts.concurrency=10] Max parallel ticket transitions.
|
|
112
|
-
* @param {number} [opts.retries=3] Max attempts per ticket on transient errors.
|
|
113
|
-
* @param {number} [opts.retryBaseMs=500] Base for exponential backoff (attempt * base).
|
|
114
|
-
* @param {Function} [opts.notify] Optional notify function — forwarded to
|
|
115
|
-
* each `transitionTicketState` call so successful transitions fire a
|
|
116
|
-
* state-transition notification.
|
|
117
|
-
* @returns {Promise<{ transitioned: number[], skipped: number[], failed: Array<{ id: number, error: string, attempts: number }> }>}
|
|
118
|
-
*/
|
|
119
|
-
export async function batchTransitionTickets(
|
|
120
|
-
provider,
|
|
121
|
-
tickets,
|
|
122
|
-
targetLabel,
|
|
123
|
-
opts = {},
|
|
124
|
-
) {
|
|
125
|
-
const { progress, onError, notify } = opts;
|
|
126
|
-
const transitioned = [];
|
|
127
|
-
const skipped = [];
|
|
128
|
-
const failed = [];
|
|
129
|
-
|
|
130
|
-
const concurrency = opts.concurrency ?? 10;
|
|
131
|
-
const maxRetries = opts.retries ?? 3;
|
|
132
|
-
const retryBaseMs = opts.retryBaseMs ?? 500;
|
|
133
|
-
|
|
134
|
-
const processTicket = async (ticket) => {
|
|
135
|
-
if (ticket.labels.includes(targetLabel)) {
|
|
136
|
-
progress?.('TICKETS', ` #${ticket.id} already ${targetLabel} — skipped`);
|
|
137
|
-
skipped.push(ticket.id);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (
|
|
141
|
-
targetLabel !== STATE_LABELS.DONE &&
|
|
142
|
-
ticket.labels.includes(STATE_LABELS.DONE)
|
|
143
|
-
) {
|
|
144
|
-
progress?.('TICKETS', ` #${ticket.id} already done — skipped`);
|
|
145
|
-
skipped.push(ticket.id);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
let lastErr;
|
|
149
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
150
|
-
try {
|
|
151
|
-
// Story #1795 — thread the already-hydrated ticket through
|
|
152
|
-
// `opts.ticketSnapshot` so `transitionTicketState` skips both
|
|
153
|
-
// round-trips (notify snapshot + label merge). `batchTransitionTickets`
|
|
154
|
-
// is the single hot caller of this seam — every wave-close passes
|
|
155
|
-
// its full task list through here.
|
|
156
|
-
await transitionTicketState(provider, ticket.id, targetLabel, {
|
|
157
|
-
notify,
|
|
158
|
-
ticketSnapshot: ticket,
|
|
159
|
-
});
|
|
160
|
-
progress?.(
|
|
161
|
-
'TICKETS',
|
|
162
|
-
` #${ticket.id} → ${targetLabel} ✅${attempt > 1 ? ` (after ${attempt} attempts)` : ''}`,
|
|
163
|
-
);
|
|
164
|
-
transitioned.push(ticket.id);
|
|
165
|
-
return;
|
|
166
|
-
} catch (err) {
|
|
167
|
-
lastErr = err;
|
|
168
|
-
if (attempt >= maxRetries || !isRetryableError(err)) break;
|
|
169
|
-
const delay = retryBaseMs * 2 ** (attempt - 1);
|
|
170
|
-
progress?.(
|
|
171
|
-
'TICKETS',
|
|
172
|
-
` #${ticket.id} transient error (${err.message}); retrying in ${delay}ms (${attempt}/${maxRetries - 1})`,
|
|
173
|
-
);
|
|
174
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
failed.push({
|
|
178
|
-
id: ticket.id,
|
|
179
|
-
error: lastErr?.message ?? String(lastErr),
|
|
180
|
-
attempts: maxRetries,
|
|
181
|
-
});
|
|
182
|
-
if (onError) onError(ticket.id, lastErr);
|
|
183
|
-
else
|
|
184
|
-
Logger.error(` #${ticket.id} → FAILED: ${lastErr?.message ?? lastErr}`);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// Process in batches to avoid overwhelming the API with concurrent requests.
|
|
188
|
-
for (let i = 0; i < tickets.length; i += concurrency) {
|
|
189
|
-
const batch = tickets.slice(i, i + concurrency);
|
|
190
|
-
await Promise.all(batch.map(processTicket));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return { transitioned, skipped, failed };
|
|
194
|
-
}
|