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.
Files changed (253) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. 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
- }