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.
- package/.agents/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-entry-sync.js +469 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/story-body.js +12 -59
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +0 -2
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +42 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +12 -3
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -1,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
|
-
}
|