mandrel 2.36.0 → 2.38.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 +16 -16
- package/.agents/agents/plan-critic.md +1 -1
- package/.agents/docs/SDLC.md +40 -40
- package/.agents/docs/configuration.md +41 -16
- package/.agents/docs/execution-reference.md +3 -3
- package/.agents/docs/quality-gates.md +2 -2
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +5 -5
- package/.agents/rules/changelog-style.md +3 -3
- package/.agents/rules/ci-remediation.md +1 -1
- package/.agents/rules/git-conventions-reference.md +4 -4
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +9 -9
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/audit-to-stories.js +2 -2
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-doc-links.js +32 -4
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/check-test-temp-hygiene.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +1 -1
- package/.agents/scripts/deliver-light.js +10 -10
- package/.agents/scripts/deliver-recover.js +1 -1
- package/.agents/scripts/drain-pending-cleanup.js +19 -2
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bdd-step-index.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
- package/.agents/scripts/lib/cli-args.js +1 -3
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/command-header.js +1 -1
- package/.agents/scripts/lib/config/delivery-routing.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
- package/.agents/scripts/lib/config-settings-schema.js +49 -9
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +2 -2
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +12 -12
- package/.agents/scripts/lib/findings/route-finding.js +2 -2
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/label-constants.js +2 -2
- package/.agents/scripts/lib/label-taxonomy.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/onboard/init-tail.js +4 -4
- package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +4 -4
- package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
- package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
- package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
- package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
- package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
- package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
- package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
- package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/story-adjacency.js +2 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/util/concurrent-map.js +1 -1
- package/.agents/scripts/lib/util/parse-id-list.js +1 -1
- package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/lib/workflow-closure.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/lint-issue-body.js +3 -3
- package/.agents/scripts/plan-context.js +4 -4
- package/.agents/scripts/plan-critics.js +2 -2
- package/.agents/scripts/plan-persist.js +2 -2
- package/.agents/scripts/plan-run-epilogue.js +2 -2
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/tickets.js +114 -21
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/resolve-stories.js +3 -3
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/run-lint.js +1 -1
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +2 -4
- package/.agents/scripts/single-story-init.js +10 -6
- package/.agents/scripts/stories-wave-tick.js +8 -8
- package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/skills/skills.index.json +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
- package/.agents/workflows/audit-data-model.md +1 -1
- package/.agents/workflows/audit-documentation.md +2 -2
- package/.agents/workflows/audit-to-stories.md +11 -11
- package/.agents/workflows/git-deliver.md +4 -4
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/audit-lens-core.md +1 -1
- package/.agents/workflows/helpers/code-review.md +4 -4
- package/.agents/workflows/helpers/deliver-light.md +26 -26
- package/.agents/workflows/helpers/deliver-reference.md +15 -7
- package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
- package/.agents/workflows/helpers/deliver-story.md +6 -7
- package/.agents/workflows/helpers/diagnose.md +2 -2
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/helpers/plan-reference.md +9 -9
- package/.agents/workflows/helpers/qa-core.md +8 -8
- package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
- package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
- package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
- package/.agents/workflows/memory-consolidate.md +4 -4
- package/.agents/workflows/prototype.md +3 -3
- package/.agents/workflows/qa-assist.md +12 -12
- package/.agents/workflows/qa-explore.md +6 -6
- package/.agents/workflows/qa-run.md +1 -1
- package/README.md +7 -7
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/doctor.js +1 -1
- package/lib/cli/init.js +1 -1
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +7 -3
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- package/.agents/scripts/lib/orchestration/recut.js +0 -56
|
@@ -31,7 +31,7 @@ workflow states only the `/qa-explore`-specific phases (Plan / Capture) plus a
|
|
|
31
31
|
Constraints delta.
|
|
32
32
|
|
|
33
33
|
> **When to run**: ad-hoc agent-driven exploration of a freshly delivered Story
|
|
34
|
-
> or Feature, a regression sweep over a risky surface before `/deliver`, or a
|
|
34
|
+
> or Feature, a regression sweep over a risky surface before `/mandrel-deliver`, or a
|
|
35
35
|
> structured agent-driven bug-hunt captured into a triageable ledger.
|
|
36
36
|
>
|
|
37
37
|
> **Skills**: `core/qa-coverage-mapping`, `stack/qa/qa-explore-driving`
|
|
@@ -198,14 +198,14 @@ For each observation the agent makes while driving:
|
|
|
198
198
|
Route the captured ledger through the shared classify → route → disposition →
|
|
199
199
|
promote procedure in [`helpers/qa-core.md`](helpers/qa-core.md), with the
|
|
200
200
|
operator deciding each `file` / `defer` / `dismiss` and every write
|
|
201
|
-
operator-gated. `file` findings are promoted through `/plan` (never a raw
|
|
201
|
+
operator-gated. `file` findings are promoted through `/mandrel-plan` (never a raw
|
|
202
202
|
Issue); `defer` carries an item forward as backlog; `dismiss` marks it
|
|
203
203
|
non-actionable.
|
|
204
204
|
|
|
205
205
|
After triage, write the updated dispositions back to the ledger (still under
|
|
206
206
|
`temp/qa/`), and summarize: items captured, the driving method used, classes,
|
|
207
207
|
routes (`new`/`update-existing`/`duplicate`/`regression-of-closed`), the
|
|
208
|
-
Stories (`/plan --seed-file`) promoted, and the deferred rolling backlog a
|
|
208
|
+
Stories (`/mandrel-plan --seed-file`) promoted, and the deferred rolling backlog a
|
|
209
209
|
resumed session will pick up.
|
|
210
210
|
|
|
211
211
|
---
|
|
@@ -238,11 +238,11 @@ the `/qa-explore`-specific deltas are:
|
|
|
238
238
|
|
|
239
239
|
## See also
|
|
240
240
|
|
|
241
|
-
- [`/plan`](plan.md) — the planning pipeline Triage chains into for a
|
|
241
|
+
- [`/mandrel-plan`](mandrel-plan.md) — the planning pipeline Triage chains into for a
|
|
242
242
|
`file`-dispositioned finding. The plan→deliver hard stop is preserved.
|
|
243
243
|
- [`/qa-assist`](qa-assist.md) — the human-led sibling that enriches a single
|
|
244
|
-
operator observation and triages through the same `/plan` handoff.
|
|
244
|
+
operator observation and triages through the same `/mandrel-plan` handoff.
|
|
245
245
|
- [`/audit-to-stories`](audit-to-stories.md) — the precedent for the
|
|
246
|
-
findings → `/plan` handoff and the shared fingerprint-footer dedup contract.
|
|
246
|
+
findings → `/mandrel-plan` handoff and the shared fingerprint-footer dedup contract.
|
|
247
247
|
- [`helpers/qa-core.md`](helpers/qa-core.md) — the shared contract/session/
|
|
248
248
|
redaction/QaLedgerItem/triage/HITL core.
|
|
@@ -26,7 +26,7 @@ the agent never invents those decisions in prose.
|
|
|
26
26
|
|
|
27
27
|
> **When to run**: during sprint testing to exercise a targeted slice of the
|
|
28
28
|
> acceptance suite (a feature, a tag expression, or a domain), for regression
|
|
29
|
-
> passes before `/deliver`, or on demand while debugging a Story's
|
|
29
|
+
> passes before `/mandrel-deliver`, or on demand while debugging a Story's
|
|
30
30
|
> user-visible behavior in a live browser.
|
|
31
31
|
>
|
|
32
32
|
> **Skills**: `stack/qa/gherkin-authoring`, `stack/qa/playwright-bdd`
|
package/README.md
CHANGED
|
@@ -30,26 +30,26 @@ provisions both as part of a cold start (`git init` → `gh repo create --push`
|
|
|
30
30
|
The canonical cold-start path is one command, then one slash command:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
npx mandrel init # install mandrel → sync → prompt → bootstrap → onboarding tail → /plan handoff
|
|
33
|
+
npx mandrel init # install mandrel → sync → prompt → bootstrap → onboarding tail → /mandrel-plan handoff
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
```text
|
|
37
37
|
# then, inside Claude Code (commands load from .claude/commands/):
|
|
38
|
-
/plan --seed "…" # interrogate → author one Story (default) → persist
|
|
39
|
-
/deliver <id> # story-<id> → PR → main
|
|
38
|
+
/mandrel-plan --seed "…" # interrogate → author one Story (default) → persist
|
|
39
|
+
/mandrel-deliver <id> # story-<id> → PR → main
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
`npx mandrel init` installs `mandrel` (when `./.agents/` is absent),
|
|
43
43
|
materializes it via `mandrel sync`, then asks whether to **configure now**
|
|
44
44
|
(option 1 → runs `bootstrap.js`, then the onboarding tail: stack detection,
|
|
45
|
-
docs scaffolding offer, `mandrel doctor` readiness gate, and a `/plan`
|
|
45
|
+
docs scaffolding offer, `mandrel doctor` readiness gate, and a `/mandrel-plan`
|
|
46
46
|
handoff) or stop at **just the files** (option 2 → re-run `mandrel init`
|
|
47
47
|
any time to configure). Pass `--assume-yes` for a non-interactive run that
|
|
48
48
|
proceeds straight to configure (and forwards the flag to bootstrap). When
|
|
49
49
|
`./.agents/` is already present (you ran `npm install mandrel` first), `init`
|
|
50
50
|
skips the install/sync and goes straight to the prompt. Once `mandrel init`
|
|
51
|
-
completes, you land at the `/plan` handoff — run `/plan --seed "<idea>"` to
|
|
52
|
-
author your first Story, then deliver it with `/deliver <storyId>`
|
|
51
|
+
completes, you land at the `/mandrel-plan` handoff — run `/mandrel-plan --seed "<idea>"` to
|
|
52
|
+
author your first Story, then deliver it with `/mandrel-deliver <storyId>`
|
|
53
53
|
(`story-<id>` → PR → `main`).
|
|
54
54
|
|
|
55
55
|
### Manual equivalent
|
|
@@ -164,7 +164,7 @@ npx mandrel doctor # verify the install
|
|
|
164
164
|
Mandrel's effectiveness is measured by a separate companion repo,
|
|
165
165
|
**[mandrel-bench](https://github.com/dsj1984/mandrel-bench)** — a *consumer* of
|
|
166
166
|
the published `mandrel` package. It pins a specific framework version,
|
|
167
|
-
materializes it via `mandrel sync`, and drives Mandrel's own `/plan`→`/deliver`
|
|
167
|
+
materializes it via `mandrel sync`, and drives Mandrel's own `/mandrel-plan`→`/mandrel-deliver`
|
|
168
168
|
pipeline (plus a bare-model control) over a scenario corpus. Each run is scored
|
|
169
169
|
across five dimensions — Quality, Planning fidelity, and Autonomy (what the
|
|
170
170
|
scaffolding *buys*) versus Efficiency and Overhead ratio (what it *costs*) —
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,49 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.38.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.37.0...mandrel-v2.38.0) (2026-09-04)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### ⚠ BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* the `/plan` and `/deliver` slash commands are renamed to `/mandrel-plan` and `/mandrel-deliver`. Run `mandrel sync` (or `npm run sync:commands`) to reap the old command files and project the new ones. No `/plan` or `/deliver` alias remains.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
* rename /plan and /deliver to /mandrel-plan and /mandrel-deliver ([#5126](https://github.com/dsj1984/mandrel/issues/5126)) ([#5127](https://github.com/dsj1984/mandrel/issues/5127)) ([945d8de](https://github.com/dsj1984/mandrel/commit/945d8de4a41aeca7adc89c009a0252f5bb5387b4))
|
|
28
|
+
|
|
29
|
+
## [2.37.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.36.0...mandrel-v2.37.0) (2026-09-03)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### ⚠ BREAKING CHANGES
|
|
33
|
+
|
|
34
|
+
* `single-story-close.js` now exits non-zero on `--dry-run` or `--no-evidence` instead of silently ignoring them. Neither flag ever had an effect; a caller passing one was getting a full real close.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
* cut the test suite's child-process budget: seed the e2e consumer once and move tests/e2e to its own tier, replace CLI cold-start spawns with in-process calls, and settle the isolation trade-off ([#5111](https://github.com/dsj1984/mandrel/issues/5111)) ([#5122](https://github.com/dsj1984/mandrel/issues/5122)) ([a4fbd56](https://github.com/dsj1984/mandrel/commit/a4fbd5628b8bc11c918e0ff658fe62baf078a17f))
|
|
39
|
+
* detect high-severity advisories on main between PRs, and bound the TypeScript devDependency range ([#5104](https://github.com/dsj1984/mandrel/issues/5104)) ([#5107](https://github.com/dsj1984/mandrel/issues/5107)) ([af8f1b8](https://github.com/dsj1984/mandrel/commit/af8f1b8ee28300391a3a43cca1d43ece5851ce83))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
* close five concurrency holes: sweep-lock steal and heartbeat, one lock for the merged-branch reap, atomic pending-cleanup manifest, idempotent createIssue retry, additive lease claim ([#5112](https://github.com/dsj1984/mandrel/issues/5112)) ([#5119](https://github.com/dsj1984/mandrel/issues/5119)) ([8651514](https://github.com/dsj1984/mandrel/commit/86515147381cf653c1e41c01f6405f5333826253))
|
|
45
|
+
* **deps:** bump fast-uri to 3.1.7 for four high-severity advisories ([#5103](https://github.com/dsj1984/mandrel/issues/5103)) ([06c37b1](https://github.com/dsj1984/mandrel/commit/06c37b11952fb8079027c0beceef831bed677485))
|
|
46
|
+
* **drain-pending-cleanup:** honour --no-escalate and guard advertised --no-* flags (refs [#5101](https://github.com/dsj1984/mandrel/issues/5101)) ([#5106](https://github.com/dsj1984/mandrel/issues/5106)) ([52ef3cb](https://github.com/dsj1984/mandrel/commit/52ef3cb0c546714900474c0613d78b23bc40a068))
|
|
47
|
+
* restore the suite profiler, make run-tests --help print usage, and stop three tests burning worker slots on real backoff and a live-repo scan ([#5110](https://github.com/dsj1984/mandrel/issues/5110)) ([#5115](https://github.com/dsj1984/mandrel/issues/5115)) ([dbe0742](https://github.com/dsj1984/mandrel/commit/dbe07420a7d8d3c99016968d9c15e45da64d0de2))
|
|
48
|
+
* single-story-close advertises --dry-run and --no-evidence but never wired either: retire both and reject them loudly ([#5100](https://github.com/dsj1984/mandrel/issues/5100)) ([#5102](https://github.com/dsj1984/mandrel/issues/5102)) ([2f4dd52](https://github.com/dsj1984/mandrel/commit/2f4dd52c682bcc3697f67124d6dc574e209ab204))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Performance
|
|
52
|
+
|
|
53
|
+
* stop the baseline gates doing pointless heavy work: lazy TypeScript, small-batch in-process scoring, precompiled globs, diff-scoped cyclomatic, precompiled .agentrc validator ([#5109](https://github.com/dsj1984/mandrel/issues/5109)) ([#5117](https://github.com/dsj1984/mandrel/issues/5117)) ([6b04d5e](https://github.com/dsj1984/mandrel/commit/6b04d5e7c014b4bc052afbef8fd88d85c7a6b00f))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
* collapse the tripled semver comparator and the copy-pasted migration-step scaffold in the published lib/ payload ([#5113](https://github.com/dsj1984/mandrel/issues/5113)) ([#5116](https://github.com/dsj1984/mandrel/issues/5116)) ([ae2b6d6](https://github.com/dsj1984/mandrel/commit/ae2b6d6c40b26ea6d2f94b07698867863011a659))
|
|
59
|
+
* extract one JavaScript comment stripper, bring two cyclomatic-20 CLI mains under the ceiling, and delete the orchestration payload’s dead code ([#5114](https://github.com/dsj1984/mandrel/issues/5114)) ([#5118](https://github.com/dsj1984/mandrel/issues/5118)) ([747d9a5](https://github.com/dsj1984/mandrel/commit/747d9a5bce66a6230fe6f46d132b3e8aacbc5a09))
|
|
60
|
+
|
|
18
61
|
## [2.36.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.35.0...mandrel-v2.36.0) (2026-08-29)
|
|
19
62
|
|
|
20
63
|
|
package/lib/cli/doctor.js
CHANGED
|
@@ -122,7 +122,7 @@ export function formatClosureReport({
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Best-effort write of the doctor verdict to `temp/doctor-result.json` under
|
|
125
|
-
* the consumer root so downstream workflows (e.g. the `/plan` first-run
|
|
125
|
+
* the consumer root so downstream workflows (e.g. the `/mandrel-plan` first-run
|
|
126
126
|
* preflight) can read the last recorded verdict without re-running doctor.
|
|
127
127
|
* `temp/` is the gitignored scratch root, so neither git nor the sync prune
|
|
128
128
|
* pass ever sees the cache. Any write failure is swallowed — the cache is
|
package/lib/cli/init.js
CHANGED
|
@@ -299,7 +299,7 @@ export async function planInit({
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
// Bootstrap succeeded — run the onboarding tail: stack detection, docs
|
|
302
|
-
// scaffolding offer, doctor gate, and /plan handoff. A tail that reports
|
|
302
|
+
// scaffolding offer, doctor gate, and /mandrel-plan handoff. A tail that reports
|
|
303
303
|
// `ok: false` (the doctor gate failed) makes the whole init exit
|
|
304
304
|
// non-zero; the tail already printed its own remediation message, and the
|
|
305
305
|
// earlier install/sync/bootstrap phases' results stand as completed.
|
package/lib/cli/migrate.js
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
import {
|
|
55
55
|
migrations as defaultRegistry,
|
|
56
56
|
runMigrations as defaultRunMigrations,
|
|
57
|
+
selectStepsInRange,
|
|
57
58
|
} from '../migrations/index.js';
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -77,36 +78,97 @@ function parseOption(argv, flag) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* exactly.
|
|
81
|
+
* The envelope shape every `runMigrate` exit returns. Centralised so the four
|
|
82
|
+
* exits cannot drift in which fields they populate.
|
|
83
83
|
*
|
|
84
|
-
* @param {
|
|
85
|
-
* @returns {
|
|
84
|
+
* @param {object} fields
|
|
85
|
+
* @returns {object}
|
|
86
86
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
function migrateResult(fields) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
action: 'usage-error',
|
|
91
|
+
fromVersion: null,
|
|
92
|
+
toVersion: null,
|
|
93
|
+
dryRun: false,
|
|
94
|
+
applied: [],
|
|
95
|
+
skipped: [],
|
|
96
|
+
wouldApply: [],
|
|
97
|
+
wouldSkip: [],
|
|
98
|
+
...fields,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Both bounds are required: the runner filters `fromVersion < v <= toVersion`,
|
|
104
|
+
* so an absent bound is ambiguous rather than a sensible default.
|
|
105
|
+
*
|
|
106
|
+
* @param {{ writeErr: (s: string) => void, exit: (code: number) => void }} io
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
function reportUsageError({ writeErr, exit }) {
|
|
110
|
+
writeErr(
|
|
111
|
+
'mandrel migrate: both --from <version> and --to <version> are required.\n' +
|
|
112
|
+
' → Usage: mandrel migrate --from <version> --to <version> [--dry-run]\n',
|
|
113
|
+
);
|
|
114
|
+
exit(1);
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
/**
|
|
97
|
-
*
|
|
118
|
+
* Report the in-range steps and whether each WOULD apply or be skipped,
|
|
119
|
+
* probing a throwaway context so no step's `apply` runs and nothing on disk
|
|
120
|
+
* changes. The ordering comes from `selectStepsInRange`, the same selector the
|
|
121
|
+
* live run uses, so the preview cannot disagree with what would happen.
|
|
98
122
|
*
|
|
99
|
-
* @param {
|
|
100
|
-
*
|
|
101
|
-
*
|
|
123
|
+
* @param {{
|
|
124
|
+
* registry: Array<object>,
|
|
125
|
+
* fromVersion: string,
|
|
126
|
+
* toVersion: string,
|
|
127
|
+
* write: (s: string) => void,
|
|
128
|
+
* }} params
|
|
129
|
+
* @returns {{ wouldApply: string[], wouldSkip: string[] }}
|
|
102
130
|
*/
|
|
103
|
-
function
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
131
|
+
function previewMigrations({ registry, fromVersion, toVersion, write }) {
|
|
132
|
+
const inRange = selectStepsInRange({ registry, fromVersion, toVersion });
|
|
133
|
+
const wouldApply = [];
|
|
134
|
+
const wouldSkip = [];
|
|
135
|
+
const probeCtx = {};
|
|
136
|
+
|
|
137
|
+
write(`mandrel migrate — dry run v${fromVersion} → v${toVersion}\n`);
|
|
138
|
+
if (inRange.length === 0) {
|
|
139
|
+
write(' (no migration steps in range)\n');
|
|
108
140
|
}
|
|
109
|
-
|
|
141
|
+
for (const step of inRange) {
|
|
142
|
+
const bucket = step.detect(probeCtx) ? wouldApply : wouldSkip;
|
|
143
|
+
const verb = bucket === wouldApply ? 'would apply ' : 'would skip ';
|
|
144
|
+
bucket.push(step.version);
|
|
145
|
+
write(` ${verb} ${step.version}: ${step.description}\n`);
|
|
146
|
+
}
|
|
147
|
+
write('Dry run: no migrations applied, nothing written.\n');
|
|
148
|
+
|
|
149
|
+
return { wouldApply, wouldSkip };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {{
|
|
154
|
+
* applied: string[],
|
|
155
|
+
* fromVersion: string,
|
|
156
|
+
* toVersion: string,
|
|
157
|
+
* write: (s: string) => void,
|
|
158
|
+
* }} params
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
function reportApplied({ applied, fromVersion, toVersion, write }) {
|
|
162
|
+
if (applied.length === 0) {
|
|
163
|
+
write(
|
|
164
|
+
`mandrel migrate: no migrations to apply for v${fromVersion} → v${toVersion}.\n`,
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const plural = applied.length === 1 ? '' : 's';
|
|
169
|
+
write(
|
|
170
|
+
`✅ Applied ${applied.length} migration${plural} (v${fromVersion} → v${toVersion}).\n`,
|
|
171
|
+
);
|
|
110
172
|
}
|
|
111
173
|
|
|
112
174
|
/**
|
|
@@ -146,107 +208,51 @@ export function runMigrate({
|
|
|
146
208
|
const fromVersion = parseOption(argv, '--from');
|
|
147
209
|
const toVersion = parseOption(argv, '--to');
|
|
148
210
|
|
|
149
|
-
// --- Usage validation -----------------------------------------------------
|
|
150
|
-
// Both bounds are required: the runner filters fromVersion < v <= toVersion,
|
|
151
|
-
// so an absent bound is ambiguous rather than a sensible default.
|
|
152
211
|
if (!fromVersion || !toVersion) {
|
|
153
|
-
writeErr
|
|
154
|
-
|
|
155
|
-
' → Usage: mandrel migrate --from <version> --to <version> [--dry-run]\n',
|
|
156
|
-
);
|
|
157
|
-
exit(1);
|
|
158
|
-
return {
|
|
159
|
-
ok: false,
|
|
160
|
-
action: 'usage-error',
|
|
212
|
+
reportUsageError({ writeErr, exit });
|
|
213
|
+
return migrateResult({
|
|
161
214
|
fromVersion: fromVersion ?? null,
|
|
162
215
|
toVersion: toVersion ?? null,
|
|
163
216
|
dryRun,
|
|
164
|
-
|
|
165
|
-
skipped: [],
|
|
166
|
-
wouldApply: [],
|
|
167
|
-
wouldSkip: [],
|
|
168
|
-
};
|
|
217
|
+
});
|
|
169
218
|
}
|
|
170
219
|
|
|
171
|
-
// --- Dry run --------------------------------------------------------------
|
|
172
|
-
// Report the in-range steps and whether each WOULD apply or be skipped,
|
|
173
|
-
// probing a throwaway context so no step's apply runs and nothing on disk
|
|
174
|
-
// changes.
|
|
175
220
|
if (dryRun) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const wouldApply = [];
|
|
185
|
-
const wouldSkip = [];
|
|
186
|
-
const probeCtx = {};
|
|
187
|
-
|
|
188
|
-
write(`mandrel migrate — dry run v${fromVersion} → v${toVersion}\n`);
|
|
189
|
-
if (inRange.length === 0) {
|
|
190
|
-
write(' (no migration steps in range)\n');
|
|
191
|
-
}
|
|
192
|
-
for (const step of inRange) {
|
|
193
|
-
const willApply = step.detect(probeCtx);
|
|
194
|
-
if (willApply) {
|
|
195
|
-
wouldApply.push(step.version);
|
|
196
|
-
write(` would apply ${step.version}: ${step.description}\n`);
|
|
197
|
-
} else {
|
|
198
|
-
wouldSkip.push(step.version);
|
|
199
|
-
write(` would skip ${step.version}: ${step.description}\n`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
write('Dry run: no migrations applied, nothing written.\n');
|
|
203
|
-
|
|
204
|
-
return {
|
|
221
|
+
const { wouldApply, wouldSkip } = previewMigrations({
|
|
222
|
+
registry,
|
|
223
|
+
fromVersion,
|
|
224
|
+
toVersion,
|
|
225
|
+
write,
|
|
226
|
+
});
|
|
227
|
+
return migrateResult({
|
|
205
228
|
ok: true,
|
|
206
229
|
action: 'dry-run',
|
|
207
230
|
fromVersion,
|
|
208
231
|
toVersion,
|
|
209
232
|
dryRun: true,
|
|
210
|
-
applied: [],
|
|
211
|
-
skipped: [],
|
|
212
233
|
wouldApply,
|
|
213
234
|
wouldSkip,
|
|
214
|
-
};
|
|
235
|
+
});
|
|
215
236
|
}
|
|
216
237
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// and the per-step `migrated …` log line.
|
|
238
|
+
// The runner owns ordering, range filtering, idempotency, and the per-step
|
|
239
|
+
// `migrated …` log line.
|
|
220
240
|
const { applied, skipped } = runMigrations({
|
|
221
241
|
fromVersion,
|
|
222
242
|
toVersion,
|
|
223
243
|
ctx,
|
|
224
244
|
registry,
|
|
225
245
|
});
|
|
246
|
+
reportApplied({ applied, fromVersion, toVersion, write });
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
write(
|
|
229
|
-
`mandrel migrate: no migrations to apply for v${fromVersion} → v${toVersion}.\n`,
|
|
230
|
-
);
|
|
231
|
-
} else {
|
|
232
|
-
write(
|
|
233
|
-
`✅ Applied ${applied.length} migration${
|
|
234
|
-
applied.length === 1 ? '' : 's'
|
|
235
|
-
} (v${fromVersion} → v${toVersion}).\n`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
248
|
+
return migrateResult({
|
|
240
249
|
ok: true,
|
|
241
250
|
action: 'migrated',
|
|
242
251
|
fromVersion,
|
|
243
252
|
toVersion,
|
|
244
|
-
dryRun: false,
|
|
245
253
|
applied,
|
|
246
254
|
skipped,
|
|
247
|
-
|
|
248
|
-
wouldSkip: [],
|
|
249
|
-
};
|
|
255
|
+
});
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
/**
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// lib/migrations/helpers/retire-agentrc-key.js
|
|
2
|
+
/**
|
|
3
|
+
* The shared scaffold behind every "strip a retired `.agentrc.json` key"
|
|
4
|
+
* migration step.
|
|
5
|
+
*
|
|
6
|
+
* Retiring a config key is the framework's most common migration: a key is
|
|
7
|
+
* dropped from the runtime AJV schema, the block it lived under carries
|
|
8
|
+
* `additionalProperties: false`, and a consumer whose config still sets it
|
|
9
|
+
* hits a hard validation failure on upgrade rather than a warning. Every such
|
|
10
|
+
* step reads the config, decides whether the key is present, deletes it,
|
|
11
|
+
* prunes whatever containers that emptied, and writes the file back — the
|
|
12
|
+
* same mechanics each time, previously copy-pasted per step (jscpd recorded
|
|
13
|
+
* five of them between 48 and 58 percent duplicated).
|
|
14
|
+
*
|
|
15
|
+
* Only three things actually vary between steps, so only those three are
|
|
16
|
+
* declared:
|
|
17
|
+
*
|
|
18
|
+
* - **which config surfaces to sweep** — `.agentrc.json` alone, or that plus
|
|
19
|
+
* the gitignored `.agentrc.local.json`. `config-resolver.js` deep-merges
|
|
20
|
+
* the overlay over the base *before* the AJV gate runs, so a key surviving
|
|
21
|
+
* in the overlay fails exactly as a base one would. A step that swept only
|
|
22
|
+
* the base would report "nothing to migrate" and leave that consumer hard
|
|
23
|
+
* broken with no self-service remedy.
|
|
24
|
+
* - **which key paths are retired** — a step may retire more than one.
|
|
25
|
+
* - **how far to prune emptied ancestors** — this is deliberately per-key
|
|
26
|
+
* and not a global policy. `planning` is optional, so an emptied
|
|
27
|
+
* `planning` block is removed; `project` is required and an empty
|
|
28
|
+
* `commands` object is valid against the schema, so pruning it would be a
|
|
29
|
+
* cosmetic edit to a config the consumer owns.
|
|
30
|
+
*
|
|
31
|
+
* Builtins only, and the `fs` seam stays injectable via `ctx.fs`, because
|
|
32
|
+
* these steps run during `mandrel update` before third-party packages are
|
|
33
|
+
* guaranteed to be present.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import nodeFs from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
|
|
39
|
+
/** The committed config every consumer has. */
|
|
40
|
+
export const AGENTRC_BASE_FILENAME = '.agentrc.json';
|
|
41
|
+
|
|
42
|
+
/** The operator-owned, gitignored overlay the resolver merges over the base. */
|
|
43
|
+
const AGENTRC_LOCAL_FILENAME = '.agentrc.local.json';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Both surfaces the resolver reads — the default sweep for a retired key.
|
|
47
|
+
*
|
|
48
|
+
* Module-local: a step that wants both surfaces takes the default and names
|
|
49
|
+
* nothing, so exporting this would ship a symbol with no importer.
|
|
50
|
+
*/
|
|
51
|
+
const AGENTRC_FILENAMES = Object.freeze([
|
|
52
|
+
AGENTRC_BASE_FILENAME,
|
|
53
|
+
AGENTRC_LOCAL_FILENAME,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {unknown} ctx
|
|
58
|
+
* @param {string} filename
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function resolveAgentrcPath(ctx, filename) {
|
|
62
|
+
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
63
|
+
return path.join(projectRoot, filename);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read and parse one config surface. An absent or unparseable file is the
|
|
68
|
+
* common case (no overlay, fresh checkout), not an error.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} ctx
|
|
71
|
+
* @param {string} filename
|
|
72
|
+
* @param {typeof nodeFs} fsImpl
|
|
73
|
+
* @returns {object | null}
|
|
74
|
+
*/
|
|
75
|
+
function readAgentrcConfig(ctx, filename, fsImpl) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx, filename), 'utf8');
|
|
78
|
+
return JSON.parse(raw);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walk the container chain for a key path, returning every object along it.
|
|
86
|
+
* Returns `null` as soon as a level is missing or is not a plain object, which
|
|
87
|
+
* is what makes `hasKey` false for a config that never had the block.
|
|
88
|
+
*
|
|
89
|
+
* @param {object | null} config
|
|
90
|
+
* @param {string[]} keyPath
|
|
91
|
+
* @returns {object[] | null}
|
|
92
|
+
*/
|
|
93
|
+
function resolveContainers(config, keyPath) {
|
|
94
|
+
if (!config || typeof config !== 'object') return null;
|
|
95
|
+
const containers = [config];
|
|
96
|
+
let cursor = config;
|
|
97
|
+
for (const segment of keyPath.slice(0, -1)) {
|
|
98
|
+
const next = cursor[segment];
|
|
99
|
+
if (!next || typeof next !== 'object') return null;
|
|
100
|
+
containers.push(next);
|
|
101
|
+
cursor = next;
|
|
102
|
+
}
|
|
103
|
+
return containers;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {object | null} config
|
|
108
|
+
* @param {string[]} keyPath
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function hasKey(config, keyPath) {
|
|
112
|
+
const containers = resolveContainers(config, keyPath);
|
|
113
|
+
if (!containers) return false;
|
|
114
|
+
const leafOwner = containers[containers.length - 1];
|
|
115
|
+
return Object.hasOwn(leafOwner, keyPath[keyPath.length - 1]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Delete the leaf key, then remove up to `pruneDepth` ancestors that the
|
|
120
|
+
* deletion left empty. `pruneDepth: 0` deletes the key and nothing else.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} config
|
|
123
|
+
* @param {{ path: string[], pruneDepth?: number }} key
|
|
124
|
+
* @returns {void}
|
|
125
|
+
*/
|
|
126
|
+
function stripKey(config, key) {
|
|
127
|
+
const keyPath = key.path;
|
|
128
|
+
const containers = resolveContainers(config, keyPath);
|
|
129
|
+
if (!containers) return;
|
|
130
|
+
|
|
131
|
+
const leafOwner = containers[containers.length - 1];
|
|
132
|
+
delete leafOwner[keyPath[keyPath.length - 1]];
|
|
133
|
+
|
|
134
|
+
const pruneDepth = key.pruneDepth ?? 0;
|
|
135
|
+
for (let level = 0; level < pruneDepth; level += 1) {
|
|
136
|
+
const index = containers.length - 1 - level;
|
|
137
|
+
const emptied = containers[index];
|
|
138
|
+
const parent = containers[index - 1];
|
|
139
|
+
if (!parent || Object.keys(emptied).length > 0) break;
|
|
140
|
+
delete parent[keyPath[index - 1]];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build a migration step that strips one or more retired keys from the
|
|
146
|
+
* consumer's config surfaces.
|
|
147
|
+
*
|
|
148
|
+
* `detect` is true when any declared key is present in any swept surface;
|
|
149
|
+
* `apply` rewrites only the surfaces that actually carry one, which is what
|
|
150
|
+
* makes a repeat pass a genuine no-op rather than a reformat.
|
|
151
|
+
*
|
|
152
|
+
* @param {{
|
|
153
|
+
* version: string,
|
|
154
|
+
* description: string,
|
|
155
|
+
* filenames?: readonly string[],
|
|
156
|
+
* keys: Array<{ path: string[], pruneDepth?: number }>,
|
|
157
|
+
* }} spec
|
|
158
|
+
* @returns {{
|
|
159
|
+
* version: string,
|
|
160
|
+
* description: string,
|
|
161
|
+
* detect: (ctx?: { projectRoot?: string, fs?: typeof nodeFs }) => boolean,
|
|
162
|
+
* apply: (ctx?: { projectRoot?: string, fs?: typeof nodeFs }) => void,
|
|
163
|
+
* }}
|
|
164
|
+
*/
|
|
165
|
+
export function createRetireAgentrcKeyStep({
|
|
166
|
+
version,
|
|
167
|
+
description,
|
|
168
|
+
filenames = AGENTRC_FILENAMES,
|
|
169
|
+
keys,
|
|
170
|
+
}) {
|
|
171
|
+
const carriesRetiredKey = (config) =>
|
|
172
|
+
keys.some((key) => hasKey(config, key.path));
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
version,
|
|
176
|
+
description,
|
|
177
|
+
detect(ctx) {
|
|
178
|
+
const fsImpl = ctx?.fs ?? nodeFs;
|
|
179
|
+
return filenames.some((filename) =>
|
|
180
|
+
carriesRetiredKey(readAgentrcConfig(ctx, filename, fsImpl)),
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
apply(ctx) {
|
|
184
|
+
const fsImpl = ctx?.fs ?? nodeFs;
|
|
185
|
+
for (const filename of filenames) {
|
|
186
|
+
const config = readAgentrcConfig(ctx, filename, fsImpl);
|
|
187
|
+
if (!carriesRetiredKey(config)) continue;
|
|
188
|
+
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
if (hasKey(config, key.path)) stripKey(config, key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fsImpl.writeFileSync(
|
|
194
|
+
resolveAgentrcPath(ctx, filename),
|
|
195
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|