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.
- package/.agents/README.md +1 -1
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +2 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/schemas/agentrc.schema.json +1 -5
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
- package/.agents/scripts/agents-update-preflight.js +235 -0
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/epic-deliver-preflight.js +30 -13
- package/.agents/scripts/epic-deliver-prepare.js +40 -53
- package/.agents/scripts/epic-execute-record-wave.js +119 -133
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +13 -5
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/explain.js +0 -2
- package/.agents/scripts/lib/config/limits.js +19 -8
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-settings-schema.js +1 -2
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/maintainability-utils.js +32 -9
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -162
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
- package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
- package/.agents/scripts/lib/story-adjacency.js +14 -10
- package/.agents/scripts/lib/story-body/story-body.js +142 -65
- package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
- package/.agents/scripts/lib/wave-runner/tick.js +446 -216
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/lint-label-vocabulary.js +1 -1
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/stories-wave-tick.js +262 -161
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/agents-update.md +205 -28
- package/.agents/workflows/deliver.md +12 -9
- package/.agents/workflows/helpers/deliver-epic.md +126 -90
- package/.agents/workflows/helpers/deliver-stories.md +131 -85
- package/.agents/workflows/helpers/plan-epic.md +13 -10
- package/.agents/workflows/plan.md +1 -1
- package/README.md +20 -0
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- 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:
|
|
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:
|
|
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`, `
|
|
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
|
-
* - `
|
|
34
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
133
|
+
* dependencyDepth: number,
|
|
128
134
|
* githubApiRequests: number,
|
|
129
135
|
* claudeQuotaTokens: number,
|
|
130
136
|
* }}
|
|
131
137
|
*/
|
|
132
138
|
export function computeEstimate({
|
|
133
139
|
storyCount,
|
|
134
|
-
|
|
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(
|
|
151
|
+
if (!Number.isInteger(dependencyDepth) || dependencyDepth < 0) {
|
|
146
152
|
throw new TypeError(
|
|
147
|
-
'computeEstimate:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
['
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
143
|
+
* storyCount: number,
|
|
137
144
|
* concurrencyCap: number,
|
|
138
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
355
|
-
//
|
|
356
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
379
|
+
storyCount: openStories.length,
|
|
393
380
|
concurrencyCap,
|
|
394
|
-
|
|
381
|
+
stories,
|
|
395
382
|
checkpointInitializedAt:
|
|
396
383
|
checkpointState.startedAt ??
|
|
397
384
|
checkpointState.lastUpdatedAt ??
|