mandrel 1.68.0 → 1.70.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 (75) hide show
  1. package/.agents/README.md +1 -1
  2. package/.agents/docs/agentrc-reference.json +1 -2
  3. package/.agents/docs/configuration.md +2 -4
  4. package/.agents/docs/workflows.md +1 -1
  5. package/.agents/schemas/agentrc.schema.json +1 -5
  6. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
  7. package/.agents/scripts/agents-update-preflight.js +235 -0
  8. package/.agents/scripts/apply-quality-bootstrap.js +79 -0
  9. package/.agents/scripts/audit-labels-bootstrap.js +52 -30
  10. package/.agents/scripts/audit-to-stories.js +54 -0
  11. package/.agents/scripts/bootstrap.js +13 -3
  12. package/.agents/scripts/epic-deliver-preflight.js +30 -13
  13. package/.agents/scripts/epic-deliver-prepare.js +40 -53
  14. package/.agents/scripts/epic-execute-record-wave.js +119 -133
  15. package/.agents/scripts/generate-config-docs.js +189 -94
  16. package/.agents/scripts/lib/audit-suite/findings.js +0 -4
  17. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
  18. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
  19. package/.agents/scripts/lib/baseline-snapshot.js +163 -4
  20. package/.agents/scripts/lib/baselines/refresh-service.js +13 -5
  21. package/.agents/scripts/lib/config/baselines.js +0 -20
  22. package/.agents/scripts/lib/config/explain.js +0 -2
  23. package/.agents/scripts/lib/config/limits.js +19 -8
  24. package/.agents/scripts/lib/config/temp-paths.js +0 -31
  25. package/.agents/scripts/lib/config-settings-schema.js +1 -2
  26. package/.agents/scripts/lib/crap-utils.js +281 -0
  27. package/.agents/scripts/lib/maintainability-utils.js +32 -9
  28. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
  29. package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
  30. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
  31. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
  32. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
  33. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -162
  34. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
  35. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
  36. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
  37. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
  38. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
  39. package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
  40. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
  41. package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
  42. package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
  43. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
  44. package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
  45. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
  46. package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
  47. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
  48. package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
  49. package/.agents/scripts/lib/story-adjacency.js +14 -10
  50. package/.agents/scripts/lib/story-body/story-body.js +142 -65
  51. package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
  52. package/.agents/scripts/lib/test-tiers.js +13 -7
  53. package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
  54. package/.agents/scripts/lib/wave-runner/tick.js +446 -216
  55. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
  56. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
  57. package/.agents/scripts/lint-label-vocabulary.js +1 -1
  58. package/.agents/scripts/providers/github/issues.js +48 -0
  59. package/.agents/scripts/providers/github.js +1 -0
  60. package/.agents/scripts/stories-wave-tick.js +262 -161
  61. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
  62. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
  63. package/.agents/skills/skills.index.json +2 -2
  64. package/.agents/workflows/agents-update.md +205 -28
  65. package/.agents/workflows/deliver.md +12 -9
  66. package/.agents/workflows/helpers/deliver-epic.md +126 -90
  67. package/.agents/workflows/helpers/deliver-stories.md +131 -85
  68. package/.agents/workflows/helpers/plan-epic.md +13 -10
  69. package/.agents/workflows/plan.md +1 -1
  70. package/README.md +20 -0
  71. package/docs/CHANGELOG.md +46 -0
  72. package/lib/cli/registry.js +49 -6
  73. package/lib/cli/update.js +335 -332
  74. package/package.json +16 -11
  75. package/.agents/scripts/lib/wave-runner/wave-checkpoint.js +0 -91
@@ -2,36 +2,39 @@
2
2
  /* node:coverage ignore file */
3
3
 
4
4
  /**
5
- * epic-execute-record-wave.js — record one wave's per-Story returns,
6
- * advance the `epic-run-state` checkpoint, and re-render the unified
5
+ * epic-execute-record-wave.js — record one recorder beat's per-Story
6
+ * returns, splice each Story's terminal status into the `epic-run-state`
7
+ * checkpoint's flat per-Story `stories` map, and re-render the unified
7
8
  * `epic-run-progress` rollup on the Epic.
8
9
  *
9
- * The slash-command (`/deliver`) calls this CLI once per wave, after
10
- * its host-level Agent-tool fan-out drains. It is the only writer of the
11
- * `epic-run-progress` structured comment for the wave-completion path
12
- * there is no separate `/wave-execute` skill, no `wave-run-progress`
13
- * comment, and no separate rollup CLI. The host LLM owns wave dispatch;
14
- * this CLI owns the post-wave persistence and operator-facing summary.
10
+ * Story #4155 (Epic #4151) — the Epic `/deliver` runtime cut over from the
11
+ * wave-batch scheduler to the continuous ready-set core. This recorder lost
12
+ * its wave semantics with it: there is no `--wave` flag, no wave-level
13
+ * status aggregation, no `currentWave` advance, and no `waves[]` history. The
14
+ * tick (`lib/wave-runner/tick.js`) re-derives readiness from live labels on
15
+ * every beat, so the checkpoint only records terminal Story outcomes (for the
16
+ * auto-merge predicate, branch cleanup, and the operator rollup). The
17
+ * recorder also emits one `story.dispatch.end` per recorded Story so the
18
+ * tick's ledger-derived in-flight set and the `--check-idle` watchdog can
19
+ * clear the dispatch start/end pairing.
15
20
  *
16
21
  * 1. Parse / reconcile / verify the per-Story returns.
17
- * 2. Aggregate the wave's terminal status (complete | blocked | failed).
18
- * 3. Splice the wave outcome into `state.waves[]`, advance
19
- * `state.currentWave` on `complete`, and re-write the checkpoint.
20
- * 4. Re-render `epic-run-progress` from `state.waves[]`.
22
+ * 2. Record each Story's terminal status onto the checkpoint `stories` map.
23
+ * 3. Emit `story.dispatch.end` per recorded Story.
24
+ * 4. Re-render `epic-run-progress` from the checkpoint `stories` map.
21
25
  * 5. Print the next action for the slash-command (`dispatch-next` |
22
- * `finalize` | `halt-blocked` | `halt-failed`).
26
+ * `halt-blocked` | `halt-failed`).
23
27
  *
24
28
  * The implementation is split across three modules so the parent stays a
25
29
  * thin runner shell:
26
30
  *
27
- * - `lib/orchestration/wave-record-projection.js` — pure projection
28
- * helpers (status aggregation, rollup-row shaping, next-record
29
- * splicing, next-action classification). Re-exported from this file
30
- * so existing callers see an unchanged public surface.
31
+ * - `lib/orchestration/wave-record-projection.js` — pure helpers
32
+ * (per-Story validation / normalization, rollup-row shaping). Re-exported
33
+ * from this file so existing callers see an unchanged public surface.
31
34
  * - `lib/orchestration/wave-record-io.js` — impure helpers (ticket
32
35
  * verification, manifest title lookup, returns reconciliation).
33
36
  * - `lib/orchestration/wave-record-notifications.js` — curated webhook
34
- * emit chain for the wave boundary.
37
+ * emit chain for the recorder beat.
35
38
  */
36
39
 
37
40
  import { readFileSync } from 'node:fs';
@@ -52,14 +55,11 @@ import {
52
55
  resolveResolvedResults,
53
56
  verifyWaveResults,
54
57
  } from './lib/orchestration/wave-record-io.js';
55
- import { emitWaveBoundaryNotifications } from './lib/orchestration/wave-record-notifications.js';
58
+ import { emitRecordNotifications } from './lib/orchestration/wave-record-notifications.js';
56
59
  import {
57
- projectWaveRecord,
58
- resolveConcurrencyCap,
59
60
  selectInputFlag,
60
- validateEpicWave,
61
+ validateEpic,
61
62
  validateResults,
62
- validateResultsReturnsXor,
63
63
  } from './lib/orchestration/wave-record-projection.js';
64
64
  import { createProvider } from './lib/provider-factory.js';
65
65
  import { notify } from './notify.js';
@@ -73,29 +73,25 @@ export {
73
73
  // Re-export the pure projection surface so tests and downstream consumers
74
74
  // can keep importing from `epic-execute-record-wave.js` after the extract.
75
75
  export {
76
- aggregateWaveStatus,
77
76
  classifyParsedReturn,
78
- classifyWaveOutcome,
79
- countDoneStories,
80
- resolveConcurrencyCap,
77
+ normalizeReturnsPure,
81
78
  STORY_STATUS_TO_ROW_STATE,
82
79
  selectInputFlag,
83
80
  toRollupRow,
84
- VALID_RESULT_STATUSES,
85
81
  VALID_STORY_STATUSES,
86
- validateEpicWave,
82
+ validateEpic,
87
83
  validateResults,
88
- validateResultsReturnsXor,
89
84
  validateReturnsEntry,
90
85
  } from './lib/orchestration/wave-record-projection.js';
91
86
 
92
87
  const HELP = `Usage: node .agents/scripts/epic-execute-record-wave.js \\
93
- --epic <epicId> --wave <waveIndex> [--concurrency-cap <N>] \\
88
+ --epic <epicId> \\
94
89
  (--returns @<file>|<inline-json> | --results @<file>|<inline-json>)
95
90
 
96
- Records the wave's per-Story outcomes, advances the epic-run-state
97
- checkpoint, and upserts the unified epic-run-progress rollup on the Epic.
98
- Prints the next action for the /deliver slash command.
91
+ Records this recorder beat's per-Story outcomes onto the epic-run-state
92
+ checkpoint's flat per-Story status map and upserts the unified
93
+ epic-run-progress rollup on the Epic. Prints the next action for the
94
+ /deliver slash command.
99
95
  `;
100
96
 
101
97
  /**
@@ -135,15 +131,36 @@ export function parseInputArg(value, deps = {}) {
135
131
  }
136
132
 
137
133
  /**
138
- * End-to-end record-wave. DI-friendly: tests pass `injectedProvider` and a
134
+ * Classify the recorder beat's next action from the verified rows. Pure.
135
+ * Any failed Story → `halt-failed`; else any blocked Story → `halt-blocked`;
136
+ * else → `dispatch-next` (the host re-ticks to pick up the next ready set).
137
+ *
138
+ * @param {Array<{ status: string }>} verified
139
+ * @returns {{ status: 'complete'|'blocked'|'failed', nextAction: string, blockedStoryIds: number[] }}
140
+ */
141
+ export function classifyRecordOutcome(verified) {
142
+ const rows = Array.isArray(verified) ? verified : [];
143
+ const failed = rows.some((r) => r.status === 'failed');
144
+ const blockedStoryIds = rows
145
+ .filter((r) => r.status === 'blocked')
146
+ .map((r) => r.storyId);
147
+ if (failed) {
148
+ return { status: 'failed', nextAction: 'halt-failed', blockedStoryIds };
149
+ }
150
+ if (blockedStoryIds.length > 0) {
151
+ return { status: 'blocked', nextAction: 'halt-blocked', blockedStoryIds };
152
+ }
153
+ return { status: 'complete', nextAction: 'dispatch-next', blockedStoryIds };
154
+ }
155
+
156
+ /**
157
+ * End-to-end record beat. DI-friendly: tests pass `injectedProvider` and a
139
158
  * fully-formed `results` (or `returns`) array to skip real network reads.
140
159
  *
141
160
  * @param {{
142
161
  * epicId: number,
143
- * wave: number,
144
162
  * results?: unknown,
145
163
  * returns?: unknown,
146
- * concurrencyCap?: number,
147
164
  * cwd?: string,
148
165
  * injectedProvider?: object,
149
166
  * injectedConfig?: object,
@@ -153,18 +170,25 @@ export function parseInputArg(value, deps = {}) {
153
170
  */
154
171
  export async function runEpicExecuteRecordWave({
155
172
  epicId,
156
- wave,
157
173
  results,
158
174
  returns,
159
- concurrencyCap: concurrencyCapOverride,
160
175
  cwd,
161
176
  injectedProvider,
162
177
  injectedConfig,
163
178
  injectedNotify,
164
179
  now = () => new Date(),
165
180
  } = {}) {
166
- validateEpicWave(epicId, wave);
167
- validateResultsReturnsXor(results, returns);
181
+ validateEpic(epicId);
182
+ if (results == null && returns == null) {
183
+ throw new TypeError(
184
+ 'runEpicExecuteRecordWave: either `results` or `returns` is required',
185
+ );
186
+ }
187
+ if (results != null && returns != null) {
188
+ throw new TypeError(
189
+ 'runEpicExecuteRecordWave: pass `results` OR `returns`, not both',
190
+ );
191
+ }
168
192
 
169
193
  const config = injectedConfig ?? resolveConfig({ cwd });
170
194
  const provider = injectedProvider ?? createProvider(config);
@@ -176,25 +200,16 @@ export async function runEpicExecuteRecordWave({
176
200
  'run `node .agents/scripts/epic-deliver-prepare.js --epic <id>` first.',
177
201
  );
178
202
  }
203
+ const firstRecord = !hasRecordedStory(existing.stories);
179
204
 
180
205
  const deliverRunner = getRunners(config).deliverRunner ?? {};
181
- const concurrencyCap = resolveConcurrencyCap(
182
- concurrencyCapOverride,
183
- existing,
184
- deliverRunner,
185
- );
186
206
 
187
- // 1. Parse / reconcile the per-Story returns. `existing` is threaded so the
188
- // wave-complete-livelock recovery (Story #3907) can reconcile every
189
- // Story in `plan[wave]` from GitHub when mode B records a wave with no
190
- // child returns.
207
+ // 1. Parse / reconcile the per-Story returns.
191
208
  const { resolvedResults, parseFailures } = await resolveResolvedResults({
192
209
  provider,
193
210
  epicId,
194
- wave,
195
211
  results,
196
212
  returns,
197
- existing,
198
213
  });
199
214
 
200
215
  const validated = validateResults(resolvedResults);
@@ -209,90 +224,61 @@ export async function runEpicExecuteRecordWave({
209
224
  // 3. Cross-look manifest titles for the rollup rows.
210
225
  const titleById = await loadManifestTitleMap({ provider, epicId });
211
226
 
212
- // 4. Project the post-wave-record state. Pure: aggregates the wave
213
- // status, splices this wave's record into the prior list, derives
214
- // nextCurrentWave, and classifies the slash-command next action.
215
- const projection = projectWaveRecord({
216
- wave,
217
- verified,
218
- existing,
219
- concurrencyCap,
220
- titleById,
221
- now,
222
- });
223
-
224
- // 5. Persist the projected checkpoint.
225
- await epicRunStateStore.write({
226
- provider,
227
- epicId,
228
- state: {
229
- ...existing,
230
- currentWave: projection.nextCurrentWave,
231
- totalWaves: projection.totalWaves,
232
- waves: projection.nextWaves,
233
- },
234
- });
227
+ // 4. Record each Story's terminal status onto the checkpoint `stories`
228
+ // map. Recorded serially so the upserts do not race the same comment.
229
+ let state = existing;
230
+ for (const row of verified) {
231
+ state = await epicRunStateStore.recordStoryStatus({
232
+ provider,
233
+ epicId,
234
+ storyId: row.storyId,
235
+ status: row.status,
236
+ title: titleById.get(row.storyId),
237
+ blockerCommentId: row.blockerCommentId,
238
+ });
239
+ }
235
240
 
236
- // 5a. Emit one `story.dispatch.end` per recorded Story (Story #3900).
237
- // Closes the start/end pairing the wave-tick reconciler and the
238
- // `--check-idle` watchdog use to derive in-flight Stories. Before this
239
- // the only producer was `wave-session.js`, which the host-LLM driven
240
- // /deliver path never imports — so every dispatched Story stayed
241
- // "in-flight" forever and completed Stories tripped the watchdog.
242
- // Best-effort: a failed append must not block the wave loop.
241
+ // 5. Emit one `story.dispatch.end` per recorded Story. Closes the
242
+ // start/end pairing the tick reconciler and the `--check-idle`
243
+ // watchdog use to derive in-flight Stories. Best-effort: a failed
244
+ // append must not block the loop.
243
245
  emitWaveDispatchEnds({ epicId, verified, config });
244
246
 
245
247
  // 6. Re-render the unified `epic-run-progress` rollup from the checkpoint
246
- // state. This is the only operator-facing summary — there is no
247
- // separate per-wave structured comment.
248
+ // `stories` map. The single operator-facing summary.
248
249
  const { body: renderedBody } = await upsertEpicRunProgress({
249
250
  provider,
250
251
  epicId,
251
- waves: projection.rollupWaves,
252
- currentWave: projection.nextCurrentWave,
253
- totalWaves: projection.totalWaves,
252
+ stories: state.stories,
254
253
  startedAt: existing.startedAt,
255
254
  now,
256
255
  });
257
256
 
258
- // 7. Fire the curated webhook events for this wave boundary. Mirrors the
259
- // wave-loop emits in `lib/orchestration/epic-runner/phases/iterate-waves.js`
260
- // for the host-LLM driven /deliver path (which does not pass
261
- // through `runEpic`). Each helper is fire-and-forget webhook
262
- // misconfig or a transient Slack outage must not block the wave loop.
263
- await emitWaveBoundaryNotifications({
257
+ const { status, nextAction, blockedStoryIds } =
258
+ classifyRecordOutcome(verified);
259
+
260
+ // 7. Fire the curated webhook events for this recorder beat. Each helper
261
+ // is fire-and-forget — webhook misconfig or a transient Slack outage
262
+ // must not block the loop.
263
+ await emitRecordNotifications({
264
264
  injectedNotify,
265
265
  defaultNotify: notify,
266
266
  config,
267
267
  provider,
268
268
  epicId,
269
- wave,
270
- status: projection.status,
271
- priorWaves: projection.priorWaves,
272
- nextWaves: projection.nextWaves,
273
- titleById,
274
- totalWaves: projection.totalWaves,
275
- nextCurrentWave: projection.nextCurrentWave,
269
+ firstRecord,
270
+ stories: state.stories,
276
271
  verified,
277
- blockedStoryIds: projection.blockedStoryIds,
272
+ blockedStoryIds,
278
273
  });
279
274
 
280
- // Note (Story #3909): the per-wave dispatch-manifest refresh hop was
281
- // deleted. It re-ran the full dispatch pipeline (re-fetch every ticket,
282
- // recompute waves) on every tick only to re-render a comment nothing reads
283
- // for control flow. The manifest is written once at prepare time; the
284
- // surviving operator-facing surface is the `epic-run-progress` rollup
285
- // re-rendered above.
286
-
287
275
  const envelope = {
288
276
  epicId,
289
- wave,
290
277
  recorded: true,
291
- status: projection.status,
278
+ status,
292
279
  stories: verified.map((r) => ({ id: r.storyId, status: r.status })),
293
- blockedStoryIds: projection.blockedStoryIds,
294
- nextAction: projection.nextAction,
295
- remainingWaves: projection.remainingWaves,
280
+ blockedStoryIds,
281
+ nextAction,
296
282
  renderedBody,
297
283
  };
298
284
  if (discrepancies.length > 0) {
@@ -307,12 +293,28 @@ export async function runEpicExecuteRecordWave({
307
293
  return envelope;
308
294
  }
309
295
 
296
+ /**
297
+ * Whether the checkpoint's per-Story `stories` map already carries a
298
+ * non-`pending` (recorded) status — used to fire `epic-started` exactly
299
+ * once, on the first recorder beat. Pure helper.
300
+ *
301
+ * @param {Record<string, { status?: string }>|undefined} stories
302
+ * @returns {boolean}
303
+ */
304
+ function hasRecordedStory(stories) {
305
+ const map = stories && typeof stories === 'object' ? stories : {};
306
+ for (const rec of Object.values(map)) {
307
+ if (rec?.status && rec.status !== 'pending') return true;
308
+ }
309
+ return false;
310
+ }
311
+
310
312
  /**
311
313
  * Append one `story.dispatch.end` lifecycle record per recorded Story
312
314
  * (Story #3900). Each emit is independent and best-effort: a single failed
313
- * append is logged and swallowed so one bad record never aborts the wave
314
- * loop. The Story status taxonomy (`done`/`blocked`/`failed`) maps directly
315
- * onto the `story.dispatch.end` outcome enum.
315
+ * append is logged and swallowed so one bad record never aborts the loop.
316
+ * The Story status taxonomy (`done`/`blocked`/`failed`) maps directly onto
317
+ * the `story.dispatch.end` outcome enum.
316
318
  *
317
319
  * Exported for unit testing.
318
320
  *
@@ -378,8 +380,6 @@ export function parseArgv(argv) {
378
380
  const { values } = defineFlags(
379
381
  {
380
382
  epic: { type: 'integer', alias: 'epicId' },
381
- wave: { type: 'integer' },
382
- 'concurrency-cap': { type: 'integer' },
383
383
  results: { type: 'string', alias: 'resultsRaw' },
384
384
  returns: { type: 'string', alias: 'returnsRaw' },
385
385
  help: { type: 'boolean', short: 'h' },
@@ -393,8 +393,7 @@ export function parseArgv(argv) {
393
393
  * Orchestration body of `main` extracted as a sibling exported function so
394
394
  * the validate / dispatch / envelope-shape ladder is unit-testable without
395
395
  * spawning a process. `main` becomes a thin shell: parse → call this →
396
- * render → exit. CLI surface unchanged (same flags, same exit codes, same
397
- * stdout JSON schema).
396
+ * render → exit.
398
397
  *
399
398
  * @param {ReturnType<typeof parseArgv>} values
400
399
  * @param {{
@@ -421,23 +420,10 @@ export async function runRecordWaveCli(values, deps = {}) {
421
420
  },
422
421
  };
423
422
  }
424
- if (!Number.isInteger(values.wave) || values.wave < 0) {
425
- return {
426
- exitCode: 2,
427
- result: {
428
- kind: 'validation-error',
429
- message:
430
- '[epic-execute-record-wave] ERROR: --wave <index> is required (>= 0).',
431
- help: helpText,
432
- },
433
- };
434
- }
435
423
  const resolveInput = deps.resolveRecordInput ?? resolveRecordInput;
436
424
  const runner = deps.runRecordWave ?? runEpicExecuteRecordWave;
437
425
  const envelope = await runner({
438
426
  epicId: values.epicId,
439
- wave: values.wave,
440
- concurrencyCap: values.concurrencyCap,
441
427
  ...resolveInput(values),
442
428
  });
443
429
  return { exitCode: 0, result: { kind: 'envelope', envelope } };