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
@@ -534,6 +534,13 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
534
534
  const reposList = lists.reposList;
535
535
  const projectsList = lists.projectsList;
536
536
  const pickerOwner = (answers) => answers?.owner || owner;
537
+ // `owner` / `repo` are GitHub-side answers. When `--skip-github` suppresses
538
+ // the entire GitHub bootstrap, they are not required — this lets a
539
+ // non-interactive `--assume-yes --skip-github` run materialize and configure
540
+ // a fresh non-git directory (no inferable remote) without hard-failing on
541
+ // `missing required answers: owner, repo`. With GitHub bootstrap active they
542
+ // remain required (the target repo must be resolvable).
543
+ const skipGithub = Boolean(flags?.['skip-github']);
537
544
  return [
538
545
  {
539
546
  key: 'owner',
@@ -541,7 +548,7 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
541
548
  env: 'GH_OWNER',
542
549
  message: '\n\nGitHub repo owner',
543
550
  default: defaults.owner,
544
- required: true,
551
+ required: !skipGithub,
545
552
  validate: (v) =>
546
553
  /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(v) ? null : 'Invalid GitHub owner',
547
554
  },
@@ -567,7 +574,7 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
567
574
  pickerMessage:
568
575
  'GitHub repo name - Select existing or press ENTER to create',
569
576
  default: defaults.repo,
570
- required: true,
577
+ required: !skipGithub,
571
578
  picker: {
572
579
  list: (answers) => {
573
580
  if (Array.isArray(reposList) && reposList.length > 0)
@@ -957,7 +964,10 @@ export async function collectAndConfirm(state) {
957
964
  });
958
965
  if (missing.length > 0) {
959
966
  Logger.error(
960
- `[Bootstrap] missing required answers: ${missing.join(', ')}`,
967
+ `[Bootstrap] missing required answers: ${missing.join(', ')}. ` +
968
+ 'Pass them as flags (e.g. `--owner <name> --repo <name>`), or run ' +
969
+ 'with `--skip-github` to configure the files/local setup only and ' +
970
+ 'wire GitHub later.',
961
971
  );
962
972
  return { ok: false, exit: 1 };
963
973
  }
@@ -8,7 +8,7 @@
8
8
  * fan-out and surfaces the result to the operator on two channels:
9
9
  *
10
10
  * 1. A JSON envelope on stdout (always) with the keys
11
- * `storyCount`, `installCostSeconds`, `waveCount`,
11
+ * `storyCount`, `installCostSeconds`, `dependencyDepth`,
12
12
  * `githubApiRequests`, `claudeQuotaTokens`, plus `breaches`
13
13
  * (the non-empty subset of `delivery.preflight.max*` thresholds the
14
14
  * estimate exceeds).
@@ -30,8 +30,14 @@
30
30
  * the comment can reason about the numbers):
31
31
  *
32
32
  * - `storyCount` = number of child `type::story` tickets.
33
- * - `waveCount` = computed wave DAG length (matches the
34
- * dispatch manifest).
33
+ * - `dependencyDepth` = longest dependency chain through the Story
34
+ * DAG (Story #4155). The ready-set runtime has
35
+ * no wave barrier, so wall-clock is bounded
36
+ * below by this depth, not by a wave count: a
37
+ * depth-1 Epic (all Stories independent) can run
38
+ * fully parallel, while a depth-N chain forces N
39
+ * sequential beats regardless of cap. Computed
40
+ * as the dependency-DAG layer count.
35
41
  * - `installCostSeconds` = `storyCount * perStoryInstallSeconds`
36
42
  * (default 45s, override via
37
43
  * `--per-story-install-seconds`). Models a
@@ -115,7 +121,7 @@ const DEFAULTS = Object.freeze({
115
121
  *
116
122
  * @param {{
117
123
  * storyCount: number,
118
- * waveCount: number,
124
+ * dependencyDepth: number,
119
125
  * perStoryInstallSeconds?: number,
120
126
  * perStoryApiRequests?: number,
121
127
  * perStoryClaudeTokens?: number,
@@ -124,14 +130,14 @@ const DEFAULTS = Object.freeze({
124
130
  * @returns {{
125
131
  * storyCount: number,
126
132
  * installCostSeconds: number,
127
- * waveCount: number,
133
+ * dependencyDepth: number,
128
134
  * githubApiRequests: number,
129
135
  * claudeQuotaTokens: number,
130
136
  * }}
131
137
  */
132
138
  export function computeEstimate({
133
139
  storyCount,
134
- waveCount,
140
+ dependencyDepth,
135
141
  perStoryInstallSeconds = DEFAULTS.perStoryInstallSeconds,
136
142
  perStoryApiRequests = DEFAULTS.perStoryApiRequests,
137
143
  perStoryClaudeTokens = DEFAULTS.perStoryClaudeTokens,
@@ -142,15 +148,15 @@ export function computeEstimate({
142
148
  'computeEstimate: storyCount must be a non-negative integer',
143
149
  );
144
150
  }
145
- if (!Number.isInteger(waveCount) || waveCount < 0) {
151
+ if (!Number.isInteger(dependencyDepth) || dependencyDepth < 0) {
146
152
  throw new TypeError(
147
- 'computeEstimate: waveCount must be a non-negative integer',
153
+ 'computeEstimate: dependencyDepth must be a non-negative integer',
148
154
  );
149
155
  }
150
156
  return {
151
157
  storyCount,
152
158
  installCostSeconds: storyCount * perStoryInstallSeconds,
153
- waveCount,
159
+ dependencyDepth,
154
160
  githubApiRequests: baseApiRequests + storyCount * perStoryApiRequests,
155
161
  claudeQuotaTokens: storyCount * perStoryClaudeTokens,
156
162
  };
@@ -167,7 +173,14 @@ export function computeEstimate({
167
173
  export function detectBreaches(estimate, thresholds) {
168
174
  const mapping = [
169
175
  { key: 'storyCount', observedKey: 'storyCount', maxKey: 'maxStories' },
170
- { key: 'waveCount', observedKey: 'waveCount', maxKey: 'maxWaves' },
176
+ // Story #4155 `dependencyDepth` replaces the retired `waveCount`
177
+ // estimate; it is still gated by the `maxWaves` config threshold (the
178
+ // published config key is left unchanged for contract stability).
179
+ {
180
+ key: 'dependencyDepth',
181
+ observedKey: 'dependencyDepth',
182
+ maxKey: 'maxWaves',
183
+ },
171
184
  {
172
185
  key: 'installCostSeconds',
173
186
  observedKey: 'installCostSeconds',
@@ -213,7 +226,7 @@ export function renderPreflightBody({
213
226
  lines.push('| --- | ---: | ---: |');
214
227
  const rows = [
215
228
  ['storyCount', estimate.storyCount, thresholds.maxStories],
216
- ['waveCount', estimate.waveCount, thresholds.maxWaves],
229
+ ['dependencyDepth', estimate.dependencyDepth, thresholds.maxWaves],
217
230
  [
218
231
  'installCostSeconds',
219
232
  estimate.installCostSeconds,
@@ -292,7 +305,11 @@ export async function runPreflight({
292
305
  state = await runBuildWaveDagPhase(ctx, {}, state);
293
306
 
294
307
  const storyCount = Array.isArray(state.stories) ? state.stories.length : 0;
295
- const waveCount = Array.isArray(state.waves) ? state.waves.length : 0;
308
+ // Dependency depth = the dependency-DAG layer count (the longest chain of
309
+ // `blocked by` edges). `runBuildWaveDagPhase` already computes this layering
310
+ // as `state.waves`, so its length is the depth even though the ready-set
311
+ // runtime no longer dispatches by wave (Story #4155).
312
+ const dependencyDepth = Array.isArray(state.waves) ? state.waves.length : 0;
296
313
 
297
314
  // Persist the snapshot/DAG envelope so `epic-deliver-prepare.js` can
298
315
  // reuse it instead of re-walking the hierarchy. The cache key is a
@@ -317,7 +334,7 @@ export async function runPreflight({
317
334
 
318
335
  const estimate = computeEstimate({
319
336
  storyCount,
320
- waveCount,
337
+ dependencyDepth,
321
338
  perStoryInstallSeconds,
322
339
  perStoryApiRequests,
323
340
  perStoryClaudeTokens,
@@ -5,17 +5,25 @@
5
5
  * epic-deliver-prepare.js — Step 0/1 of the operator-driven `/deliver`.
6
6
  *
7
7
  * Composes the existing engine phases that the in-process epic-runner used to
8
- * call sequentially, but does NOT dispatch any waves. The CLI is the single
8
+ * call sequentially, but does NOT dispatch any Stories. The CLI is the single
9
9
  * point at which the slash-command captures:
10
10
  *
11
11
  * 1. The Epic ticket snapshot (`runSnapshotPhase`).
12
- * 2. The wave DAG (`runBuildWaveDagPhase`) computed from every child Story.
12
+ * 2. The story DAG (`runBuildWaveDagPhase`) computed from every child Story
13
+ * used here only to enumerate the open Story set and run the
14
+ * concurrency-hazard gate; the ready-set runtime re-derives readiness
15
+ * from live labels on every `tick`, so the prepare no longer persists a
16
+ * wave grouping.
13
17
  * 3. The seeded `epic-run-state` checkpoint (`epic-run-state-store.initialize`)
14
- * idempotent, so re-running prepare against a partially-driven Epic
15
- * preserves the original `startedAt`.
16
- * 4. The per-wave dispatch plan (`StoryLauncher.planWave`) a deterministic
17
- * list of `{ storyId, worktree }` entries that the slash command feeds
18
- * into N parallel `Agent` tool calls per wave.
18
+ * in the per-Story-status shape (Story #4155): a flat
19
+ * `stories: { [storyId]: { status: 'pending' } }` map plus the GLOBAL
20
+ * in-flight `concurrencyCap`. Idempotent re-running prepare against a
21
+ * partially-driven Epic preserves the original `startedAt` and every
22
+ * already-recorded Story status (it never resets recorded progress).
23
+ * 4. The dispatch hint (`StoryLauncher.planWave`) — a deterministic list of
24
+ * `{ storyId, worktree }` entries the slash command uses to resolve
25
+ * per-Story worktree paths. The ready-set `tick` selects which of these
26
+ * to dispatch on each beat; the prepare only enumerates the set.
19
27
  *
20
28
  * Stdout is a single JSON envelope so the slash command can parse without
21
29
  * re-reading any tickets.
@@ -37,7 +45,6 @@ import {
37
45
  } from './lib/orchestration/epic-deliver-lease-guard.js';
38
46
  import {
39
47
  initialize as initializeEpicRunState,
40
- reconcileResumePointer,
41
48
  write as writeEpicRunState,
42
49
  } from './lib/orchestration/epic-run-state-store.js';
43
50
  import {
@@ -133,9 +140,9 @@ function resolveGitUserEmail(cwd) {
133
140
  * }} args
134
141
  * @returns {Promise<{
135
142
  * epicId: number,
136
- * totalWaves: number,
143
+ * storyCount: number,
137
144
  * concurrencyCap: number,
138
- * plan: Array<{ wave: number, stories: Array<{ storyId: number, title: string, worktree?: string }> }>,
145
+ * stories: Array<{ storyId: number, title: string, worktree?: string }>,
139
146
  * checkpointInitializedAt: string,
140
147
  * }>}
141
148
  */
@@ -334,64 +341,44 @@ export async function runEpicDeliverPrepare({
334
341
  ignoreConcurrencyHazards,
335
342
  });
336
343
 
337
- const totalWaves = state.waves.length;
344
+ // Flatten the wave-DAG into the open Story set. The ready-set runtime
345
+ // re-derives readiness from live labels on every tick, so the checkpoint
346
+ // stores only the Story set in scope (seeded at `pending`) and the global
347
+ // in-flight cap — no wave grouping, no `currentWave`, no `totalWaves`.
348
+ const openStories = state.waves.flat();
338
349
  const checkpointState = await initializeEpicRunState({
339
350
  provider,
340
351
  epicId,
341
- totalWaves,
352
+ storyIds: openStories,
342
353
  concurrencyCap,
343
354
  });
344
355
 
356
+ // Resolve per-Story worktree paths via the launcher so the slash command
357
+ // has a deterministic `{ storyId, worktree, title }` list to seed Agent
358
+ // dispatch from. This is a dispatch *hint* — the ready-set tick decides
359
+ // which Stories to dispatch on each beat; the prepare only enumerates them.
345
360
  const launcher = new StoryLauncher({ concurrencyCap });
346
- const plan = state.waves.map((stories, index) => ({
347
- wave: index,
348
- stories: launcher.planWave(stories).map((entry, i) => ({
349
- ...entry,
350
- title: stories[i]?.title ?? '',
351
- })),
361
+ const stories = launcher.planWave(openStories).map((entry, i) => ({
362
+ ...entry,
363
+ title: openStories[i]?.title ?? '',
352
364
  }));
353
365
 
354
- // Persist the plan onto the checkpoint so `wave-tick.js` (which reads
355
- // state.plan as `Array<Array<{ id|storyId, title?, worktree? }>>`) can
356
- // resolve the next wave's stories. Without this write the tick reports
357
- // every wave as `wave-complete: empty` and the delivery stalls.
358
- const tickPlan = plan.map((wave) => wave.stories);
359
-
360
- // Story #3358 — reconcile the resume pointer against the recomputed
361
- // plan. On a resumed Epic, `build-wave-dag.js` drops the already-merged
362
- // (closed) Stories, so the recomputed plan is shorter and re-indexed
363
- // from 0. The preserved `currentWave`/`waves[]` reference the *old*
364
- // index space; left untouched, `wave-tick.js` would index
365
- // `plan[currentWave]` into the new plan and dispatch the wrong wave.
366
- // Prepare owns the `plan` field, so it owns the pointer that indexes
367
- // into it. When the plan changed, reset the pointer to 0 and drop the
368
- // stale history; when it is byte-identical (idempotent re-prepare),
369
- // preserve in-flight progress verbatim.
370
- const { currentWave, waves } = reconcileResumePointer(
371
- checkpointState,
372
- checkpointState.plan,
373
- tickPlan,
374
- );
375
-
376
- // Persist the `--ignore-concurrency-hazards` flag on the checkpoint
377
- // so retro tooling can flag a run that shipped despite an outstanding
378
- // hazard (the warning above is one-shot; the checkpoint is durable).
379
- const checkpointPayload = {
380
- ...checkpointState,
381
- plan: tickPlan,
382
- currentWave,
383
- waves,
384
- };
366
+ // Persist the `--ignore-concurrency-hazards` flag on the checkpoint so
367
+ // retro tooling can flag a run that shipped despite an outstanding hazard
368
+ // (the warning above is one-shot; the checkpoint is durable).
385
369
  if (gate.bypassed) {
386
- checkpointPayload.ignoreConcurrencyHazards = true;
370
+ await writeEpicRunState({
371
+ provider,
372
+ epicId,
373
+ state: { ...checkpointState, ignoreConcurrencyHazards: true },
374
+ });
387
375
  }
388
- await writeEpicRunState({ provider, epicId, state: checkpointPayload });
389
376
 
390
377
  return {
391
378
  epicId,
392
- totalWaves,
379
+ storyCount: openStories.length,
393
380
  concurrencyCap,
394
- plan,
381
+ stories,
395
382
  checkpointInitializedAt:
396
383
  checkpointState.startedAt ??
397
384
  checkpointState.lastUpdatedAt ??