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,309 +0,0 @@
1
- // .agents/scripts/lib/orchestration/lifecycle/bus.js
2
- /**
3
- * Lifecycle event bus — sequential awaited mediator.
4
- *
5
- * The bus is the only authorized emitter of typed lifecycle events. It is a
6
- * mediator (not pub/sub): listeners run in registration order, awaited,
7
- * and a throw in any listener short-circuits the remaining listeners and
8
- * propagates to the caller. There is no Promise.all over listener arrays
9
- * — that pattern breaks repeatability and is forbidden by the lint rule in
10
- * `biome.json`.
11
- *
12
- * Wildcard subscriptions (`bus.on('*', fn)`) are permitted for trace /
13
- * heartbeat observers. They MUST NOT perform side effects on the system
14
- * under orchestration; the firewall against state-mutating imports in
15
- * wildcard listeners is documented in
16
- * `.agents/scripts/lib/orchestration/lifecycle/listeners/README.md`.
17
- *
18
- * Schema validation: `emit()` validates the payload against the schema at
19
- * `.agents/schemas/lifecycle/<event>.schema.json` BEFORE invoking any
20
- * listener. Validation failures throw immediately; no `emitted` ledger
21
- * record is written, no listener runs. This guarantees that every record
22
- * in the ledger carries a payload conformant to its event schema.
23
- *
24
- * The bus does NOT write the ledger directly. `LedgerWriter` registers as
25
- * the first listener on every event and handles persistence. This keeps
26
- * the bus a pure mediator and makes the ledger boundary easy to fake in
27
- * tests.
28
- */
29
-
30
- import { readFileSync } from 'node:fs';
31
- import path from 'node:path';
32
- import { fileURLToPath } from 'node:url';
33
-
34
- import Ajv2020 from 'ajv/dist/2020.js';
35
- import addFormats from 'ajv-formats';
36
-
37
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
- const DEFAULT_SCHEMA_DIR = path.resolve(
39
- __dirname,
40
- '..',
41
- '..',
42
- '..',
43
- '..',
44
- 'schemas',
45
- 'lifecycle',
46
- );
47
-
48
- /**
49
- * Compile a validator for a single event by loading its schema lazily. The
50
- * result is cached per Bus instance so repeated emits of the same event
51
- * don't re-parse JSON.
52
- */
53
- function buildValidator(ajv, schemaDir, event) {
54
- const schemaPath = path.join(schemaDir, `${event}.schema.json`);
55
- const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
56
- return ajv.compile(schema);
57
- }
58
-
59
- /**
60
- * Format AJV error array into a single-line message suitable for a thrown
61
- * Error. Reviewers prefer a compact summary over a stringified blob.
62
- */
63
- function formatAjvErrors(errors) {
64
- if (!Array.isArray(errors) || errors.length === 0) return 'invalid payload';
65
- return errors
66
- .map((e) => `${e.instancePath || '/'} ${e.message}`.trim())
67
- .join('; ');
68
- }
69
-
70
- export class Bus {
71
- /**
72
- * @param {object} [opts]
73
- * @param {string} [opts.schemaDir] absolute path to the lifecycle schema
74
- * directory. Defaults to `.agents/schemas/lifecycle/` resolved against
75
- * this module's location. Tests pass a fixture directory.
76
- */
77
- constructor(opts = {}) {
78
- this._schemaDir = opts.schemaDir ?? DEFAULT_SCHEMA_DIR;
79
- this._ajv = new Ajv2020({ allErrors: true });
80
- addFormats(this._ajv);
81
- /** @type {Map<string, Array<{ fn: Function, wildcard: boolean }>>} */
82
- this._listeners = new Map();
83
- /** @type {Array<{ fn: Function, wildcard: boolean }>} */
84
- this._wildcards = [];
85
- /** @type {Map<string, Function>} cached AJV validators. */
86
- this._validators = new Map();
87
- this._nextSeqId = 1;
88
- /**
89
- * Hooks that run AFTER schema validation + seqId assignment but
90
- * BEFORE any listener. LedgerWriter installs an `onEmitted` hook
91
- * here so its `emitted` ledger line lands on disk before downstream
92
- * listeners execute (the resume contract depends on this).
93
- *
94
- * Hooks are NOT regular listeners and intentionally not exposed via
95
- * `bus.on()` — they are a privileged seam used by the ledger writer
96
- * and (later) the resume coordinator. Each hook receives the same
97
- * `{ event, seqId, payload }` context shape a listener sees.
98
- *
99
- * Hooks run sequentially with await, in registration order. A throw
100
- * from a hook is treated like a thrown listener — it propagates to
101
- * the caller and short-circuits everything after it.
102
- *
103
- * @type {Array<(ctx: {event: string, seqId: number, payload: object}) => unknown | Promise<unknown>>}
104
- */
105
- this._onEmittedHooks = [];
106
- /**
107
- * Hooks that run AFTER every listener resolves successfully — the
108
- * `completed` boundary. LedgerWriter installs an `onCompleted` hook
109
- * to write its `completed` ledger line. Same execution contract as
110
- * `_onEmittedHooks` (sequential, awaited, registration order).
111
- *
112
- * @type {Array<(ctx: {event: string, seqId: number, payload: object}) => unknown | Promise<unknown>>}
113
- */
114
- this._onCompletedHooks = [];
115
- /**
116
- * Hooks that run when a listener throws — the `failed` boundary.
117
- * They receive the same context PLUS the error and the inferred
118
- * listener name (best-effort; `bus` does not currently annotate
119
- * which listener threw, but the LedgerWriter hook tags `unknown`
120
- * when no annotation is present).
121
- *
122
- * @type {Array<(ctx: {event: string, seqId: number, payload: object, listener: string, error: Error}) => unknown | Promise<unknown>>}
123
- */
124
- this._onFailedHooks = [];
125
- }
126
-
127
- /**
128
- * Privileged seam — install a hook that runs after schema validation
129
- * and seqId assignment but BEFORE any listener for the event. Used by
130
- * LedgerWriter to land the `emitted` ledger record on disk before any
131
- * downstream side effect runs (resume contract).
132
- */
133
- onEmitted(fn) {
134
- if (typeof fn !== 'function') {
135
- throw new TypeError('Bus.onEmitted: hook must be a function');
136
- }
137
- this._onEmittedHooks.push(fn);
138
- }
139
-
140
- /**
141
- * Privileged seam — install a hook that runs AFTER every listener for
142
- * the event resolves. Used by LedgerWriter to land the `completed`
143
- * ledger record.
144
- */
145
- onCompleted(fn) {
146
- if (typeof fn !== 'function') {
147
- throw new TypeError('Bus.onCompleted: hook must be a function');
148
- }
149
- this._onCompletedHooks.push(fn);
150
- }
151
-
152
- /**
153
- * Privileged seam — install a hook that runs when a listener throws.
154
- * Used by LedgerWriter to land the `failed` ledger record before
155
- * re-propagating.
156
- */
157
- onFailed(fn) {
158
- if (typeof fn !== 'function') {
159
- throw new TypeError('Bus.onFailed: hook must be a function');
160
- }
161
- this._onFailedHooks.push(fn);
162
- }
163
-
164
- /**
165
- * Register a listener. Use `event === '*'` for a wildcard observer; the
166
- * `wildcard: true` flag is set on the listener record so the lint rule
167
- * (and downstream reviewers) can identify observer-only listeners.
168
- *
169
- * Returns an unsubscribe function for symmetry with conventional emitter
170
- * APIs; the runner doesn't currently use it, but tests do.
171
- */
172
- on(event, fn) {
173
- if (typeof fn !== 'function') {
174
- throw new TypeError('Bus.on: listener must be a function');
175
- }
176
- if (typeof event !== 'string' || event.length === 0) {
177
- throw new TypeError('Bus.on: event must be a non-empty string');
178
- }
179
- const record = { fn, wildcard: event === '*' };
180
- if (event === '*') {
181
- this._wildcards.push(record);
182
- return () => {
183
- this._wildcards = this._wildcards.filter((r) => r !== record);
184
- };
185
- }
186
- const list = this._listeners.get(event) ?? [];
187
- list.push(record);
188
- this._listeners.set(event, list);
189
- return () => {
190
- const next = (this._listeners.get(event) ?? []).filter(
191
- (r) => r !== record,
192
- );
193
- if (next.length === 0) this._listeners.delete(event);
194
- else this._listeners.set(event, next);
195
- };
196
- }
197
-
198
- /**
199
- * Get (and cache) the compiled AJV validator for a given event.
200
- */
201
- _getValidator(event) {
202
- let validator = this._validators.get(event);
203
- if (!validator) {
204
- validator = buildValidator(this._ajv, this._schemaDir, event);
205
- this._validators.set(event, validator);
206
- }
207
- return validator;
208
- }
209
-
210
- /**
211
- * Emit a typed event.
212
- *
213
- * Contract (Tech Spec § Bus contract):
214
- * 1. validate payload (throw before any listener runs on mismatch);
215
- * 2. assign monotonic per-run seqId;
216
- * 3. invoke registered named listeners sequentially with await;
217
- * 4. invoke wildcard listeners sequentially with await AFTER named
218
- * listeners (observer order);
219
- * 5. propagate the first thrown error.
220
- *
221
- * The bus does NOT write the ledger directly; LedgerWriter is wired in
222
- * by the runner as the first registered named listener for every event.
223
- *
224
- * @param {string} event
225
- * @param {object} payload
226
- * @returns {Promise<{ seqId: number }>}
227
- */
228
- async emit(event, payload) {
229
- if (typeof event !== 'string' || event.length === 0) {
230
- throw new TypeError('Bus.emit: event must be a non-empty string');
231
- }
232
- const validator = this._getValidator(event);
233
- const ok = validator(payload);
234
- if (!ok) {
235
- const err = new Error(
236
- `Bus.emit: schema validation failed for "${event}": ${formatAjvErrors(validator.errors)}`,
237
- );
238
- err.code = 'BUS_SCHEMA_VALIDATION';
239
- err.event = event;
240
- err.ajvErrors = validator.errors;
241
- throw err;
242
- }
243
- const seqId = this._nextSeqId;
244
- this._nextSeqId += 1;
245
- const context = { event, seqId, payload };
246
- // Privileged onEmitted hooks run BEFORE any listener — gives
247
- // LedgerWriter the seam to land the `emitted` line on disk before
248
- // any downstream side effect (resume contract).
249
- for (const hook of this._onEmittedHooks) {
250
- await hook(context);
251
- }
252
- const named = this._listeners.get(event) ?? [];
253
- try {
254
- for (const record of named) {
255
- await record.fn(context);
256
- }
257
- for (const record of this._wildcards) {
258
- await record.fn(context);
259
- }
260
- } catch (err) {
261
- // Failed boundary — let writers/observers persist before we
262
- // propagate. Listener name annotation is best-effort: when the
263
- // error carries `.listener`, we honor it; otherwise we tag
264
- // 'unknown' so the ledger record still validates against
265
- // `ledger-record.schema.json` (which requires a `listener` field
266
- // on failed records).
267
- const listener =
268
- (err && typeof err.listener === 'string' && err.listener) || 'unknown';
269
- const failedCtx = { ...context, listener, error: err };
270
- for (const hook of this._onFailedHooks) {
271
- // Hook errors are intentionally swallowed here so we don't
272
- // mask the originating listener error. The runner only sees the
273
- // original throw.
274
- try {
275
- await hook(failedCtx);
276
- } catch (hookErr) {
277
- // Surface to stderr so a misbehaving writer doesn't go silent,
278
- // but do not propagate — original error wins.
279
- process.stderr.write(
280
- `[Bus.onFailed] hook threw while recording failure for "${event}" (seqId=${seqId}): ${hookErr.message}\n`,
281
- );
282
- }
283
- }
284
- throw err;
285
- }
286
- // Completed boundary — writers persist `completed` after every
287
- // listener resolves.
288
- for (const hook of this._onCompletedHooks) {
289
- await hook(context);
290
- }
291
- return { seqId };
292
- }
293
-
294
- /**
295
- * Inspect the next seqId without emitting. Useful for tests asserting
296
- * monotonicity across multiple bus instances or after replay.
297
- */
298
- peekNextSeqId() {
299
- return this._nextSeqId;
300
- }
301
- }
302
-
303
- /**
304
- * Convenience factory. Lets the runner own bus construction without
305
- * importing the class name directly.
306
- */
307
- export function createBus(opts = {}) {
308
- return new Bus(opts);
309
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * emit-loop-tick.js — Story #4287 (Epic #4284).
3
- *
4
- * Programmatic helper that emits a single `loop.tick` lifecycle event
5
- * THROUGH the lifecycle bus so a host-driven loop (e.g. a `/loop`-style
6
- * recurring command or a long-running poll) lands a per-pass record in
7
- * the on-disk ledger the `/deliver` idle watchdog already scans. The
8
- * record is what keeps a host loop from running silently: each round
9
- * appends an inspectable `emitted` line a reconciler can read for
10
- * forward-progress evidence.
11
- *
12
- * A host loop is not bound to a Story tier, so `loop.tick` carries a
13
- * free-form `loopName`, a monotonic `round` counter, the loop's
14
- * configured `cadence` label, and a per-round `status`. It is now the only
15
- * forward-progress beat: the Story-scoped `story.heartbeat` it was once
16
- * contrasted against was Epic-scoped by construction, could never fire under
17
- * v2 (which has no Epics), and was deleted (A22).
18
- *
19
- * Bus path (Story acceptance: "Emitting a loop.tick event THROUGH the
20
- * lifecycle bus appends a record to the per-run ledger"): this helper
21
- * constructs a `Bus`, registers a `LedgerWriter` against it, and calls
22
- * `bus.emit('loop.tick', payload)`. The bus validates the payload against
23
- * `loop.tick.schema.json` before any listener runs, and the
24
- * LedgerWriter's privileged `onEmitted` hook lands the `emitted` record
25
- * on disk — exactly the same persistence path every other lifecycle
26
- * event flows through. The helper does NOT bypass the bus with a direct
27
- * `appendFileSync`; routing through the bus is what gives the record its
28
- * schema-validated, seqId-stamped guarantee.
29
- *
30
- * Schema contract (loop.tick.schema.json):
31
- * { event, loopName, round, cadence, status, timestamp }
32
- *
33
- * The schema declares `additionalProperties: false`, so this emitter's
34
- * signature is deliberately narrow: only the schema-allowed fields are
35
- * accepted. `status` is one of running|done|blocked.
36
- *
37
- * Ledger path resolution: a caller supplies EITHER an explicit
38
- * `ledgerPath` (the host-loop case — the loop owns where its ledger
39
- * lives) OR an `epicId`, in which case the canonical
40
- * `runLedgerPath(epicId)` is used so an Epic-scoped loop's ticks land
41
- * in the same `temp/run-<id>/lifecycle.ndjson` the rest of the run
42
- * reads. Exactly one of the two MUST be supplied.
43
- */
44
-
45
- import path from 'node:path';
46
- import { fileURLToPath } from 'node:url';
47
-
48
- import { runLedgerPath } from '../../config/temp-paths.js';
49
- import { createBus } from './bus.js';
50
- import { createLedgerWriter } from './ledger-writer.js';
51
-
52
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
53
- const SCHEMA_DIR = path.resolve(
54
- __dirname,
55
- '..',
56
- '..',
57
- '..',
58
- '..',
59
- 'schemas',
60
- 'lifecycle',
61
- );
62
-
63
- const VALID_STATUSES = new Set(['running', 'done', 'blocked']);
64
-
65
- /**
66
- * Parse `temp/run-<id>/lifecycle.ndjson` (or any
67
- * `<dir>/run-<id>/lifecycle.ndjson`) back into `{ tempRoot, epicId }`
68
- * so a LedgerWriter — which is constructed from `{ epicId, tempRoot }`
69
- * rather than a raw path — can be bound to the supplied ledger path.
70
- *
71
- * The LedgerWriter intentionally re-derives the ledger path from its
72
- * `tempRoot` + `epicId` (so it can recreate the directory if a listener
73
- * moves it mid-run), so we decompose the path the caller gave us into
74
- * those two parts here.
75
- *
76
- * @param {string} ledgerPath
77
- * @returns {{ tempRoot: string, epicId: number }}
78
- */
79
- function decomposeLedgerPath(ledgerPath) {
80
- const runDir = path.dirname(ledgerPath);
81
- const tempRoot = path.dirname(runDir);
82
- const runDirName = path.basename(runDir);
83
- const m = /^run-(\d+)$/.exec(runDirName);
84
- if (!m) {
85
- throw new Error(
86
- `emitLoopTick: ledgerPath does not match <tempRoot>/run-<id>/lifecycle.ndjson layout (got ${ledgerPath})`,
87
- );
88
- }
89
- const epicId = Number.parseInt(m[1], 10);
90
- return { tempRoot, epicId };
91
- }
92
-
93
- /**
94
- * Emit exactly one `loop.tick` event through the lifecycle bus, landing
95
- * an `emitted` (and `completed`) NDJSON record in the resolved ledger.
96
- *
97
- * @param {object} opts
98
- * @param {string} opts.loopName Free-form loop identifier (non-empty).
99
- * @param {number} opts.round Monotonic pass counter (integer >= 0).
100
- * @param {string} opts.cadence Configured interval label, e.g. '5m'.
101
- * @param {string} [opts.status='running']
102
- * One of running|done|blocked.
103
- * @param {string} [opts.timestamp] ISO-8601 wall clock. Defaults to now().
104
- * @param {number} [opts.epicId] When supplied (and no `ledgerPath`),
105
- * the canonical `runLedgerPath(epicId)`
106
- * is used for the ledger.
107
- * @param {object} [opts.config] Optional resolved config for tempRoot
108
- * (only consulted on the `epicId` path).
109
- * @param {string} [opts.ledgerPath] Explicit ledger path (host-loop case).
110
- * Mutually exclusive with `epicId`.
111
- * @returns {Promise<{ ledgerPath: string, payload: object, seqId: number }>}
112
- */
113
- export async function emitLoopTick(opts) {
114
- const {
115
- loopName,
116
- round,
117
- cadence,
118
- status = 'running',
119
- timestamp = new Date().toISOString(),
120
- epicId,
121
- config,
122
- ledgerPath: ledgerPathOverride,
123
- } = opts ?? {};
124
-
125
- if (typeof loopName !== 'string' || loopName.length === 0) {
126
- throw new Error('emitLoopTick: loopName must be a non-empty string');
127
- }
128
- if (!Number.isInteger(round) || round < 0) {
129
- throw new Error('emitLoopTick: round must be a non-negative integer');
130
- }
131
- if (typeof cadence !== 'string' || cadence.length === 0) {
132
- throw new Error('emitLoopTick: cadence must be a non-empty string');
133
- }
134
- if (!VALID_STATUSES.has(status)) {
135
- throw new Error(
136
- `emitLoopTick: status "${status}" must be one of: ${[...VALID_STATUSES].join(', ')}`,
137
- );
138
- }
139
-
140
- const hasEpicId = epicId !== undefined;
141
- const hasLedgerPath = ledgerPathOverride !== undefined;
142
- if (hasEpicId === hasLedgerPath) {
143
- throw new Error('emitLoopTick: supply exactly one of epicId or ledgerPath');
144
- }
145
-
146
- let ledgerPath;
147
- if (hasLedgerPath) {
148
- if (
149
- typeof ledgerPathOverride !== 'string' ||
150
- ledgerPathOverride.length === 0
151
- ) {
152
- throw new Error('emitLoopTick: ledgerPath must be a non-empty string');
153
- }
154
- ledgerPath = ledgerPathOverride;
155
- } else {
156
- if (!Number.isInteger(epicId) || epicId < 1) {
157
- throw new Error('emitLoopTick: epicId must be a positive integer');
158
- }
159
- ledgerPath = runLedgerPath(epicId, config);
160
- }
161
-
162
- const payload = {
163
- event: 'loop.tick',
164
- loopName,
165
- round,
166
- cadence,
167
- status,
168
- timestamp,
169
- };
170
-
171
- // Route through the bus so the payload is schema-validated and the
172
- // LedgerWriter's privileged onEmitted hook persists the record — the
173
- // same path every lifecycle event flows through.
174
- const { tempRoot, epicId: ledgerEpicId } = decomposeLedgerPath(ledgerPath);
175
- const bus = createBus({ schemaDir: SCHEMA_DIR });
176
- const writer = createLedgerWriter({ epicId: ledgerEpicId, tempRoot });
177
- writer.register(bus);
178
-
179
- const { seqId } = await bus.emit('loop.tick', payload);
180
- return { ledgerPath: writer.ledgerPath, payload, seqId };
181
- }