mandrel 2.31.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 (250) 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/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -1,229 +0,0 @@
1
- // .agents/scripts/lib/orchestration/lifecycle/ledger-writer.js
2
- /**
3
- * LedgerWriter — append-only NDJSON persister for the lifecycle bus.
4
- *
5
- * Registered as the first named listener on every event by the runner
6
- * factory. Each successful emit produces exactly one `emitted` + one
7
- * `completed` line; a listener throw produces `emitted` + `failed`.
8
- *
9
- * The writer is a thin wrapper around `fs.appendFileSync` because:
10
- * - the bus is strictly sequential, so a synchronous append cannot race;
11
- * - resume semantics depend on the ledger being on disk before any
12
- * downstream listener runs (`emitted` lands before listener `N`
13
- * executes; a crash mid-listener leaves an `emitted`-without-
14
- * `completed` line that resume detects);
15
- * - synchronous I/O makes failure modes obvious (a thrown EACCES is
16
- * observable immediately and propagates through the bus).
17
- *
18
- * Secret denylist: payload keys named in `SECRET_KEY_DENY_LIST` are
19
- * stripped before write. This is defense-in-depth: payloads should never
20
- * carry secrets to begin with, but the strip means a future contributor
21
- * who accidentally adds `{ token: ... }` to an event payload can't leak
22
- * it to the on-disk ledger (which we treat as an artifact safe to attach
23
- * to PR comments).
24
- */
25
-
26
- import { appendFileSync, mkdirSync } from 'node:fs';
27
- import path from 'node:path';
28
- import { runTempDir } from '../../config/temp-paths.js';
29
-
30
- /**
31
- * Static deny-list pulled from the Tech Spec § Security & Privacy. Keys
32
- * are matched case-insensitively against own enumerable keys of the
33
- * top-level payload AND of any nested object.
34
- */
35
- export const SECRET_KEY_DENY_LIST = Object.freeze([
36
- 'token',
37
- 'password',
38
- 'secret',
39
- 'apikey',
40
- 'webhookurl',
41
- ]);
42
-
43
- const SECRET_DENY_SET = new Set(SECRET_KEY_DENY_LIST);
44
-
45
- /**
46
- * Recursively strip deny-listed keys from an arbitrary value. Arrays and
47
- * primitives are passed through; objects produce a new object with the
48
- * deny-listed keys omitted. The original input is not mutated.
49
- *
50
- * Cyclic structures aren't expected in event payloads (the schema layer
51
- * forbids them by construction), so we don't carry a `seen` set.
52
- */
53
- export function stripSecrets(value) {
54
- if (Array.isArray(value)) {
55
- return value.map(stripSecrets);
56
- }
57
- if (value && typeof value === 'object') {
58
- const out = {};
59
- for (const [k, v] of Object.entries(value)) {
60
- if (SECRET_DENY_SET.has(k.toLowerCase())) continue;
61
- out[k] = stripSecrets(v);
62
- }
63
- return out;
64
- }
65
- return value;
66
- }
67
-
68
- /**
69
- * Produce the canonical ISO-8601 wall-clock string for a ledger record.
70
- * Pulled out so tests can fake `Date.now()` without mocking the global.
71
- */
72
- function nowIso(now = Date.now) {
73
- return new Date(now()).toISOString();
74
- }
75
-
76
- /**
77
- * LedgerWriter binds to a single Epic's temp directory. The runner
78
- * constructs one per Epic run (and reuses it across resume).
79
- *
80
- * Usage:
81
- * const writer = new LedgerWriter({ epicId: 2172, tempRoot: 'temp' });
82
- * writer.register(bus);
83
- */
84
- export class LedgerWriter {
85
- /**
86
- * @param {object} opts
87
- * @param {number} opts.epicId
88
- * @param {string} opts.tempRoot - absolute or repo-relative path; the
89
- * writer resolves `temp/run-<id>/lifecycle.ndjson` underneath.
90
- * @param {() => number} [opts.now] - injectable clock for tests.
91
- */
92
- constructor(opts) {
93
- if (!opts || !Number.isInteger(opts.epicId) || opts.epicId <= 0) {
94
- throw new TypeError(
95
- 'LedgerWriter: opts.epicId must be a positive integer',
96
- );
97
- }
98
- if (typeof opts.tempRoot !== 'string' || opts.tempRoot.length === 0) {
99
- throw new TypeError(
100
- 'LedgerWriter: opts.tempRoot must be a non-empty string',
101
- );
102
- }
103
- this._epicId = opts.epicId;
104
- this._tempRoot = opts.tempRoot;
105
- this._epicDir = runTempDir(this._epicId, {
106
- project: { paths: { tempRoot: this._tempRoot } },
107
- });
108
- this._ledgerPath = path.join(this._epicDir, 'lifecycle.ndjson');
109
- this._now = typeof opts.now === 'function' ? opts.now : Date.now;
110
- }
111
-
112
- /**
113
- * Resolved on-disk path for the NDJSON ledger. Exposed for tests and
114
- * for the TraceLogger which renders the companion markdown from it.
115
- */
116
- get ledgerPath() {
117
- return this._ledgerPath;
118
- }
119
-
120
- /**
121
- * Directory housing the ledger and (later) the companion markdown.
122
- */
123
- get epicDir() {
124
- return this._epicDir;
125
- }
126
-
127
- /**
128
- * Ensure the epic-scoped temp directory exists before every append.
129
- *
130
- * The naive optimization (cache "ensured" after the first call) is
131
- * unsound when a listener moves the ledger directory mid-handler —
132
- * which is exactly what the Cleaner does on Wave 8 (Story #2259):
133
- * it renames `temp/run-<id>/` under `archive/` between the listener
134
- * body and the bus's `onCompleted` hook fire-time, and the next
135
- * append (the `completed` record for the outer event) would land
136
- * in a vanished directory. `mkdirSync({recursive: true})` is
137
- * idempotent and cheap (microseconds), so we call it every time and
138
- * keep the writer robust to its own directory being moved.
139
- */
140
- _ensureDir() {
141
- mkdirSync(this._epicDir, { recursive: true });
142
- }
143
-
144
- /**
145
- * Append one NDJSON record. Internal — callers should use the bus
146
- * listener registered by `register()`.
147
- */
148
- _appendRecord(record) {
149
- this._ensureDir();
150
- appendFileSync(this._ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
151
- }
152
-
153
- /**
154
- * Build the `emitted` record for a given context. Exposed for tests so
155
- * the deny-list strip can be asserted directly.
156
- */
157
- buildEmitted({ event, seqId, payload }) {
158
- return {
159
- kind: 'emitted',
160
- seqId,
161
- ts: nowIso(this._now),
162
- event,
163
- payload: stripSecrets(payload ?? {}),
164
- };
165
- }
166
-
167
- buildCompleted({ event, seqId }) {
168
- return {
169
- kind: 'completed',
170
- seqId,
171
- ts: nowIso(this._now),
172
- event,
173
- };
174
- }
175
-
176
- buildFailed({ event, seqId, listener, error }) {
177
- const errorRecord = {
178
- name: error?.name ? String(error.name) : 'Error',
179
- message: error?.message ? String(error.message) : String(error),
180
- };
181
- if (typeof error?.stack === 'string' && error.stack.length > 0) {
182
- errorRecord.stack = error.stack;
183
- }
184
- return {
185
- kind: 'failed',
186
- seqId,
187
- ts: nowIso(this._now),
188
- event,
189
- listener,
190
- error: errorRecord,
191
- };
192
- }
193
-
194
- /**
195
- * Register this writer against the supplied bus by installing the
196
- * privileged `onEmitted` / `onCompleted` / `onFailed` hooks. The
197
- * writer is NOT a regular listener — it uses the privileged seam so
198
- * `emitted` lands BEFORE any listener side effect (resume contract)
199
- * and `completed` / `failed` land AFTER the listener phase.
200
- */
201
- register(bus) {
202
- if (
203
- !bus ||
204
- typeof bus.onEmitted !== 'function' ||
205
- typeof bus.onCompleted !== 'function' ||
206
- typeof bus.onFailed !== 'function'
207
- ) {
208
- throw new TypeError(
209
- 'LedgerWriter.register: bus must expose onEmitted/onCompleted/onFailed hooks',
210
- );
211
- }
212
- bus.onEmitted(({ event, seqId, payload }) => {
213
- this._appendRecord(this.buildEmitted({ event, seqId, payload }));
214
- });
215
- bus.onCompleted(({ event, seqId }) => {
216
- this._appendRecord(this.buildCompleted({ event, seqId }));
217
- });
218
- bus.onFailed(({ event, seqId, listener, error }) => {
219
- this._appendRecord(this.buildFailed({ event, seqId, listener, error }));
220
- });
221
- }
222
- }
223
-
224
- /**
225
- * Factory wrapper for symmetry with `createBus()`.
226
- */
227
- export function createLedgerWriter(opts) {
228
- return new LedgerWriter(opts);
229
- }
@@ -1,54 +0,0 @@
1
- # Lifecycle Listeners
2
-
3
- Each listener in this directory subscribes to one or more lifecycle bus
4
- events and performs a single side effect. The full close-tail roster and
5
- event taxonomy live in
6
- [`docs/LIFECYCLE.md`](https://github.com/dsj1984/mandrel/blob/main/docs/LIFECYCLE.md)
7
- — that document is the SSOT. This README only indexes the **files that still
8
- live in this folder**.
9
-
10
- ## Files here
11
-
12
- - [`watcher.js`](./watcher.js) — the CI-poll loop (`watchPrToTerminal`)
13
- driven by `pr-watch-with-update.js`.
14
-
15
- `merge-watcher.js` was deleted in Story #4545: the Epic-era `MergeWatcher`
16
- listener had no production caller after the v2.0.0 Story-only cutover. The
17
- poll defaults and `deriveChecksStatus` the live close path did import from it
18
- now live in
19
- [`lib/orchestration/merge-poll.js`](../../merge-poll.js), a home the close
20
- path owns.
21
-
22
- Other close-tail side effects (ledger write, finalize/PR open, automerge
23
- arm, branch cleanup, label transition) are owned by the Story delivery
24
- path (`helpers/deliver-story` / `single-story-close.js`) rather than a
25
- `buildDefaultListenerChain` factory in this directory.
26
-
27
- ## Idempotency contract
28
-
29
- Listeners MUST be idempotent on `(event, seqId)`. The bus may invoke a
30
- listener twice for the same seqId during the resume window (when an
31
- `emitted` ledger line landed but the matching `completed` did not). The
32
- canonical pattern is a per-instance `Set<seqId>` checked at the top of
33
- the listener body; the second invocation returns early without mutating
34
- external state.
35
-
36
- ## Side-effect firewall
37
-
38
- Listeners MAY:
39
-
40
- - read tickets via the injected `provider`,
41
- - write tickets via the injected `transitionTicketState`,
42
- - upsert structured comments via the injected `upsertStructuredComment`,
43
- - append to per-run ledger / signals files under `tempRoot`.
44
-
45
- Listeners MUST NOT:
46
-
47
- - `bus.emit()` from inside a listener body (sequential mediator
48
- contract — the bus cannot re-enter safely),
49
- - import runner state directly,
50
- - mutate cross-cutting globals.
51
-
52
- Trace observers (`bus.on('*', fn)`) live under
53
- `lib/orchestration/lifecycle/trace-logger.js` and are subject to the
54
- same firewall, plus a stricter no-IO rule.
@@ -1,344 +0,0 @@
1
- // .agents/scripts/lib/orchestration/lifecycle/trace-logger.js
2
- /**
3
- * TraceLogger — renders the human-readable `lifecycle.md` companion
4
- * from the canonical NDJSON ledger.
5
- *
6
- * The companion is a strict projection of the ledger: re-rendering the
7
- * same ledger produces byte-identical Markdown (modulo wall-clock `ts`
8
- * formatting). Editing the companion does NOT affect resume; only the
9
- * NDJSON ledger is canonical. This is repeatability AC #12.
10
- *
11
- * `render(ledger)` is the pure function consumers should call.
12
- * `TraceLogger.register(bus, writerLedgerPath)` installs the wildcard
13
- * observer + the on-write side that keeps the companion in sync after
14
- * every emit, but it does so by re-reading the NDJSON file and calling
15
- * `render()` — there is no in-memory drift.
16
- */
17
-
18
- import { readFileSync, writeFileSync } from 'node:fs';
19
- import path from 'node:path';
20
-
21
- /**
22
- * Format an ISO-8601 timestamp as HH:MM:SS for the per-event line. The
23
- * `(durationMs)` chunk is computed from the gap between `emitted` and
24
- * `completed` (or `failed`) of the same seqId.
25
- */
26
- function formatClock(iso) {
27
- // The ledger record schema requires ISO date-time strings (validated
28
- // up-stream); a defensive `Date` parse here is just for resilience.
29
- const d = new Date(iso);
30
- if (Number.isNaN(d.getTime())) return '??:??:??';
31
- const hh = String(d.getUTCHours()).padStart(2, '0');
32
- const mm = String(d.getUTCMinutes()).padStart(2, '0');
33
- const ss = String(d.getUTCSeconds()).padStart(2, '0');
34
- return `${hh}:${mm}:${ss}`;
35
- }
36
-
37
- /**
38
- * Phase header derived from event name. The companion is grouped by
39
- * phase so operators can scan to the section that interests them. The
40
- * mapping is stable across runs.
41
- */
42
- const PHASE_BY_PREFIX = Object.freeze({
43
- 'story.dispatch': 'Waves',
44
- 'story.merged': 'Waves',
45
- 'story.blocked': 'Waves',
46
- 'pr.created': 'Finalize',
47
- 'notification.emitted': 'Notifications',
48
- 'checkpoint.written': 'Checkpoint',
49
- });
50
-
51
- function phaseFor(eventName) {
52
- // Match longest prefix first so `story.dispatch.start` resolves before
53
- // `story.dispatch` would match an unrelated `story.*` block.
54
- const keys = Object.keys(PHASE_BY_PREFIX).sort((a, b) => b.length - a.length);
55
- for (const k of keys) {
56
- if (eventName === k || eventName.startsWith(`${k}.`)) {
57
- return PHASE_BY_PREFIX[k];
58
- }
59
- }
60
- return 'Other';
61
- }
62
-
63
- /**
64
- * Render the payload summary chunk for a per-event line. We keep it
65
- * short: keys + scalar values, no nested object dumps (the canonical
66
- * NDJSON ledger is the place to recover full payloads). This matches
67
- * the Tech Spec spec: "payload-summary".
68
- */
69
- function summarizePayload(payload) {
70
- if (!payload || typeof payload !== 'object') return '';
71
- const parts = [];
72
- for (const [k, v] of Object.entries(payload)) {
73
- if (v == null) continue;
74
- if (
75
- typeof v === 'string' ||
76
- typeof v === 'number' ||
77
- typeof v === 'boolean'
78
- ) {
79
- parts.push(`${k}=${v}`);
80
- } else if (Array.isArray(v)) {
81
- parts.push(`${k}=[${v.length}]`);
82
- } else if (typeof v === 'object') {
83
- const keys = Object.keys(v);
84
- parts.push(`${k}={${keys.length}}`);
85
- }
86
- }
87
- return parts.join(' ');
88
- }
89
-
90
- /**
91
- * Parse an NDJSON ledger string into an array of records. Blank lines
92
- * and trailing whitespace are tolerated; malformed lines throw with
93
- * line number so the operator can locate the corruption.
94
- */
95
- export function parseLedger(text) {
96
- const out = [];
97
- const lines = String(text || '').split('\n');
98
- for (let i = 0; i < lines.length; i += 1) {
99
- const line = lines[i].trim();
100
- if (line.length === 0) continue;
101
- try {
102
- out.push(JSON.parse(line));
103
- } catch (_err) {
104
- throw new Error(
105
- `lifecycle ledger: malformed JSON on line ${i + 1}: ${line.slice(0, 80)}`,
106
- );
107
- }
108
- }
109
- return out;
110
- }
111
-
112
- /**
113
- * Pure render of a parsed ledger to Markdown. Same input → byte-identical
114
- * output (modulo `ts` field formatting, which is wall-clock by design).
115
- *
116
- * Layout (mirroring Tech Spec § Human-readable companion):
117
- * # Lifecycle — epic <id>
118
- *
119
- * ## <Phase>
120
- * HH:MM:SS event.name (durationMs) payload-summary
121
- * ...
122
- *
123
- * ## Summary
124
- * - Events: N
125
- * - Failed: N
126
- * - …
127
- */
128
- /**
129
- * Index the ledger records into the two maps `render` needs: the `emitted`
130
- * record per seqId and the terminal (`completed`/`failed`) record per seqId.
131
- * Story #4075 — extracted from `render` so the orchestrating body stays flat.
132
- */
133
- function indexLedgerRecords(records) {
134
- const emittedBySeq = new Map();
135
- const terminalBySeq = new Map();
136
- for (const rec of records) {
137
- if (!rec || typeof rec !== 'object') continue;
138
- if (rec.kind === 'emitted') emittedBySeq.set(rec.seqId, rec);
139
- else if (rec.kind === 'completed' || rec.kind === 'failed')
140
- terminalBySeq.set(rec.seqId, rec);
141
- }
142
- return { emittedBySeq, terminalBySeq };
143
- }
144
-
145
- /**
146
- * Compute the `(durationMs)` / `(pending)` chunk for one emitted event,
147
- * given its terminal record (or undefined when still in flight).
148
- */
149
- export function formatDurationChunk(emit, terminal) {
150
- if (!terminal) return '(pending)';
151
- const start = new Date(emit.ts).getTime();
152
- const end = new Date(terminal.ts).getTime();
153
- if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
154
- return `(${end - start}ms)`;
155
- }
156
- return '';
157
- }
158
-
159
- /**
160
- * Render a single per-event line for the phase section.
161
- */
162
- function formatEventLine(emit, terminal) {
163
- const failedMarker =
164
- terminal && terminal.kind === 'failed' ? ' ⚠️ FAILED' : '';
165
- const parts = [
166
- formatClock(emit.ts),
167
- emit.event,
168
- formatDurationChunk(emit, terminal),
169
- summarizePayload(emit.payload),
170
- ].filter(Boolean);
171
- return parts.join(' ') + failedMarker;
172
- }
173
-
174
- /**
175
- * Group emitted events into ordered phase buckets, each carrying its
176
- * rendered per-event lines. Phase order is first-seen by ascending seqId.
177
- */
178
- function buildPhaseLines(emittedBySeq, terminalBySeq) {
179
- const phaseOrder = [];
180
- const phaseLines = new Map();
181
- for (const emit of [...emittedBySeq.values()].sort(
182
- (a, b) => a.seqId - b.seqId,
183
- )) {
184
- const phase = phaseFor(emit.event);
185
- if (!phaseLines.has(phase)) {
186
- phaseLines.set(phase, []);
187
- phaseOrder.push(phase);
188
- }
189
- phaseLines
190
- .get(phase)
191
- .push(formatEventLine(emit, terminalBySeq.get(emit.seqId)));
192
- }
193
- return { phaseOrder, phaseLines };
194
- }
195
-
196
- /**
197
- * Compute the wall-clock span (`maxEnd - minStart`) of a single phase, or
198
- * `null` when no finite span can be derived.
199
- */
200
- function computePhaseSpanMs(phase, emittedBySeq, terminalBySeq) {
201
- let minStart = Infinity;
202
- let maxEnd = -Infinity;
203
- for (const emit of emittedBySeq.values()) {
204
- if (phaseFor(emit.event) !== phase) continue;
205
- const start = new Date(emit.ts).getTime();
206
- if (Number.isFinite(start) && start < minStart) minStart = start;
207
- const terminal = terminalBySeq.get(emit.seqId);
208
- if (terminal) {
209
- const end = new Date(terminal.ts).getTime();
210
- if (Number.isFinite(end) && end > maxEnd) maxEnd = end;
211
- }
212
- }
213
- return Number.isFinite(minStart) && Number.isFinite(maxEnd)
214
- ? maxEnd - minStart
215
- : null;
216
- }
217
-
218
- /**
219
- * Build the trailing `## Summary` block lines.
220
- */
221
- function buildSummaryLines(phaseOrder, emittedBySeq, terminalBySeq) {
222
- const totalEvents = emittedBySeq.size;
223
- const failedCount = [...terminalBySeq.values()].filter(
224
- (r) => r.kind === 'failed',
225
- ).length;
226
- const phaseDurations = [];
227
- for (const phase of phaseOrder) {
228
- const spanMs = computePhaseSpanMs(phase, emittedBySeq, terminalBySeq);
229
- if (spanMs !== null) phaseDurations.push(` - ${phase}: ${spanMs}ms`);
230
- }
231
- const lines = [
232
- '## Summary',
233
- '',
234
- `- Events: ${totalEvents}`,
235
- `- Completed: ${totalEvents - failedCount}`,
236
- `- Failed: ${failedCount}`,
237
- ];
238
- if (phaseDurations.length > 0) {
239
- lines.push('- Phase durations:', ...phaseDurations);
240
- }
241
- lines.push('');
242
- return lines;
243
- }
244
-
245
- export function render(ledger, opts = {}) {
246
- const records = Array.isArray(ledger) ? ledger : parseLedger(ledger);
247
- const { emittedBySeq, terminalBySeq } = indexLedgerRecords(records);
248
- const { phaseOrder, phaseLines } = buildPhaseLines(
249
- emittedBySeq,
250
- terminalBySeq,
251
- );
252
-
253
- const epicId = opts.epicId ? `epic ${opts.epicId}` : 'epic';
254
- const lines = [`# Lifecycle — ${epicId}`, ''];
255
- for (const phase of phaseOrder) {
256
- lines.push(`## ${phase}`, '', ...phaseLines.get(phase), '');
257
- }
258
- lines.push(...buildSummaryLines(phaseOrder, emittedBySeq, terminalBySeq));
259
- return lines.join('\n');
260
- }
261
-
262
- /**
263
- * TraceLogger wires `render()` against a live bus + ledger file. It is
264
- * a wildcard observer: it does not mutate state under orchestration, it
265
- * only re-renders the companion markdown on every event.
266
- *
267
- * The wildcard-firewall rule (Tech Spec § Bus contract) requires that
268
- * trace observers do NOT import any module that mutates GitHub state,
269
- * the worktree, or the filesystem outside `temp/run-<id>/`. This
270
- * module satisfies that constraint: the only filesystem writes are to
271
- * the companion path under the same temp directory the ledger lives
272
- * in.
273
- */
274
- export class TraceLogger {
275
- /**
276
- * @param {object} opts
277
- * @param {string} opts.ledgerPath - absolute path to the NDJSON ledger
278
- * the bus is writing (matches `LedgerWriter.ledgerPath`).
279
- * @param {number} [opts.epicId] - included in the companion header.
280
- */
281
- constructor(opts) {
282
- if (
283
- !opts ||
284
- typeof opts.ledgerPath !== 'string' ||
285
- opts.ledgerPath.length === 0
286
- ) {
287
- throw new TypeError('TraceLogger: opts.ledgerPath is required');
288
- }
289
- this._ledgerPath = opts.ledgerPath;
290
- this._companionPath = path.join(
291
- path.dirname(this._ledgerPath),
292
- 'lifecycle.md',
293
- );
294
- this._epicId = opts.epicId ?? null;
295
- }
296
-
297
- get companionPath() {
298
- return this._companionPath;
299
- }
300
-
301
- /**
302
- * Re-render the companion from the on-disk ledger. Idempotent.
303
- */
304
- rerender() {
305
- let text;
306
- try {
307
- text = readFileSync(this._ledgerPath, 'utf8');
308
- } catch (err) {
309
- if (err && err.code === 'ENOENT') return; // ledger not yet written
310
- throw err;
311
- }
312
- const markdown = render(text, { epicId: this._epicId });
313
- writeFileSync(this._companionPath, markdown, 'utf8');
314
- }
315
-
316
- /**
317
- * Register as a wildcard observer. After every emit, re-read the
318
- * ledger and re-render the companion.
319
- *
320
- * The companion render is a best-effort projection: a failed render
321
- * (malformed ledger, transient write error, full disk) MUST NOT abort
322
- * the in-flight bus emit. Because wildcard listeners run inside the
323
- * `emit()` try/catch (see `bus.js`), an unguarded throw here would
324
- * short-circuit the emit and propagate to the orchestration caller.
325
- * We instead log the failure to stderr and swallow it, degrading to a
326
- * stale `lifecycle.md` companion — the canonical NDJSON ledger is
327
- * unaffected, so resume and downstream consumers are not broken.
328
- */
329
- register(bus) {
330
- if (!bus || typeof bus.on !== 'function') {
331
- throw new TypeError('TraceLogger.register: bus must expose .on()');
332
- }
333
- bus.on('*', () => {
334
- try {
335
- this.rerender();
336
- } catch (err) {
337
- const message = err && err.message ? err.message : String(err);
338
- process.stderr.write(
339
- `[TraceLogger] companion rerender failed (degrading to stale lifecycle.md): ${message}\n`,
340
- );
341
- }
342
- });
343
- }
344
- }