mandrel 2.36.0 → 2.37.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 (88) hide show
  1. package/.agents/docs/configuration.md +25 -0
  2. package/.agents/rules/testing-standards.md +56 -0
  3. package/.agents/scripts/boot-sweep.js +7 -1
  4. package/.agents/scripts/check-context-budget.js +195 -103
  5. package/.agents/scripts/check-cyclomatic.js +112 -42
  6. package/.agents/scripts/check-generated-validator.js +202 -0
  7. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  8. package/.agents/scripts/check-schema-references.js +2 -28
  9. package/.agents/scripts/drain-pending-cleanup.js +18 -1
  10. package/.agents/scripts/lib/baselines/components.js +32 -2
  11. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  12. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  13. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  14. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  15. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  16. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  17. package/.agents/scripts/lib/cli-args.js +0 -2
  18. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  19. package/.agents/scripts/lib/close-validation/process.js +1 -1
  20. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  21. package/.agents/scripts/lib/config-settings-schema.js +46 -6
  22. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  23. package/.agents/scripts/lib/cpu-pool.js +90 -10
  24. package/.agents/scripts/lib/crap-utils.js +6 -2
  25. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  26. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  27. package/.agents/scripts/lib/dependency-version.js +86 -0
  28. package/.agents/scripts/lib/duplicate-search.js +1 -1
  29. package/.agents/scripts/lib/errors/index.js +1 -1
  30. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  31. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  32. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  33. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  34. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  35. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  36. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  37. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  38. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
  39. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  40. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  41. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  42. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  43. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  44. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  45. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  46. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  47. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  48. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  49. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  50. package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
  51. package/.agents/scripts/lib/preflight-runner.js +3 -3
  52. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  53. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  54. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  55. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  56. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  57. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  58. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  59. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  60. package/.agents/scripts/lib/test-tiers.js +135 -18
  61. package/.agents/scripts/lib/transpile.js +16 -3
  62. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
  63. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  64. package/.agents/scripts/pr-watch-with-update.js +305 -137
  65. package/.agents/scripts/providers/github/tickets.js +109 -16
  66. package/.agents/scripts/quality-preview.js +162 -70
  67. package/.agents/scripts/run-test-profile.js +8 -5
  68. package/.agents/scripts/run-tests.js +79 -14
  69. package/.agents/scripts/single-story-close.js +0 -2
  70. package/.agents/scripts/single-story-init.js +5 -1
  71. package/docs/CHANGELOG.md +32 -0
  72. package/lib/cli/migrate.js +100 -94
  73. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  74. package/lib/migrations/index.js +32 -33
  75. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  76. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  77. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  78. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  79. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  80. package/package.json +7 -3
  81. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  82. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  83. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  84. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  85. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  86. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  87. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  88. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -1,79 +0,0 @@
1
- /**
2
- * close-validation/telemetry.js — gh-spawn telemetry emitter.
3
- */
4
-
5
- import { writeFile as defaultWriteFile } from 'node:fs/promises';
6
- import path from 'node:path';
7
- import { storyTempDir } from '../config/temp-paths.js';
8
- import { getSpawnCount as defaultGetSpawnCount } from '../gh-exec.js';
9
-
10
- /**
11
- * Throw-away ghSpawnCount emitter (Story #1795 / Epic #1788).
12
- *
13
- * Writes the current `gh-exec` spawn counter to
14
- * `temp/run-<id>/stories/story-<sid>/gh-spawn-count.json`.
15
- *
16
- * Story #4545 — its reader is gone: `analyze-execution.js` consumed this file
17
- * to emit a `ghSpawnCount` field on the `story-perf-summary` payload, and both
18
- * that CLI and that payload were deleted with the execution-analysis surface.
19
- * The writer itself already had no production caller before that (the
20
- * `runPostMergeClose` orchestrator named below went in the v2.0.0 cutover), so
21
- * this module is production-dead and kept alive only by its own test — the
22
- * test-importer blind spot the dead-exports ratchet cannot see. It is left in
23
- * place rather than deleted because reviving spawn telemetry against the live
24
- * close path is a decision, not a sweep.
25
- *
26
- * @param {object} opts
27
- * @param {number|string} opts.epicId
28
- * @param {number|string} opts.storyId
29
- * @param {object} [opts.config] - Resolved config bag so `tempRoot`
30
- * resolution honours the consumer's configured path.
31
- * @param {() => number} [opts.getSpawnCountFn=defaultGetSpawnCount] - Test seam.
32
- * @param {typeof defaultWriteFile} [opts.writeFileFn=defaultWriteFile] - Test seam.
33
- * @param {{ warn?: (s: string) => void }} [opts.logger] - Best-effort
34
- * failure-path logger; never throws.
35
- * @returns {Promise<{ status: 'ok'|'failed', path?: string, ghSpawnCount?: number, reason?: string }>}
36
- */
37
- export async function emitGhSpawnCount({
38
- epicId,
39
- storyId,
40
- config,
41
- getSpawnCountFn = defaultGetSpawnCount,
42
- writeFileFn = defaultWriteFile,
43
- logger,
44
- } = {}) {
45
- const eid = Number(epicId);
46
- const sid = Number(storyId);
47
- if (!Number.isInteger(eid) || eid < 1 || !Number.isInteger(sid) || sid < 1) {
48
- return { status: 'failed', reason: 'invalid-ids' };
49
- }
50
- let ghSpawnCount;
51
- try {
52
- ghSpawnCount = getSpawnCountFn();
53
- } catch (err) {
54
- logger?.warn?.(
55
- `[close-validation] gh-spawn-count read failed: ${err?.message ?? err}`,
56
- );
57
- return { status: 'failed', reason: 'counter-read-failed' };
58
- }
59
- const targetPath = path.join(
60
- storyTempDir(eid, sid, config),
61
- 'gh-spawn-count.json',
62
- );
63
- const payload = {
64
- kind: 'gh-spawn-count',
65
- epicId: eid,
66
- storyId: sid,
67
- ghSpawnCount,
68
- capturedAt: new Date().toISOString(),
69
- };
70
- try {
71
- await writeFileFn(targetPath, JSON.stringify(payload, null, 2));
72
- return { status: 'ok', path: targetPath, ghSpawnCount };
73
- } catch (err) {
74
- logger?.warn?.(
75
- `[close-validation] gh-spawn-count emit failed: ${err?.message ?? err}`,
76
- );
77
- return { status: 'failed', reason: 'write-failed' };
78
- }
79
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * label-transitions.js — readability wrappers over `transitionTicketState`.
3
- *
4
- * These are deliberately thin: each helper names the *target* state and
5
- * forwards to the underlying SDK call. They exist so that init / close /
6
- * deliver-tail call sites read as prose ("toExecuting(provider, taskId)")
7
- * instead of forcing readers to parse the `STATE_LABELS.X` constant at
8
- * every call site.
9
- *
10
- * Not an abstraction — the underlying `transitionTicketState` remains the
11
- * authoritative single-ticket transition path. Opts are forwarded verbatim.
12
- */
13
-
14
- import { STATE_LABELS, transitionTicketState } from './ticketing.js';
15
-
16
- /** Transition a ticket to `agent::executing`. */
17
- export function toExecuting(provider, ticketId, opts) {
18
- return transitionTicketState(
19
- provider,
20
- ticketId,
21
- STATE_LABELS.EXECUTING,
22
- opts,
23
- );
24
- }
25
-
26
- /**
27
- * Transition an array of tickets to `agent::done`, in order. Each call
28
- * triggers its own cascade (via `transitionTicketState`). Failures for
29
- * individual tickets propagate — the loop aborts on the first error, so
30
- * callers that need per-ticket tolerance must catch around each id
31
- * themselves.
32
- *
33
- * @param {object} provider
34
- * @param {number[]} ticketIds
35
- * @param {object} [opts]
36
- */
37
- export async function toDone(provider, ticketIds, opts) {
38
- if (!Array.isArray(ticketIds)) {
39
- throw new TypeError('toDone: ticketIds must be an array');
40
- }
41
- for (const id of ticketIds) {
42
- await transitionTicketState(provider, id, STATE_LABELS.DONE, opts);
43
- }
44
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * Classify Stories that exist under an Epic but are absent from the frozen
3
- * dispatch manifest. Two cases:
4
- *
5
- * - "recut" — the Story carries a `<!-- recut-of: #N -->` marker whose
6
- * parent ID matches a manifest entry. These are attributable
7
- * to a manifest Story and must be satisfied alongside it.
8
- *
9
- * - "parked" — the Story is genuinely outside the manifest (carved off
10
- * mid-sprint, no recut lineage). The operator should explicitly
11
- * adopt it into the current Epic or defer it. Surfaced as a
12
- * structured comment so `/deliver` has a single checkpoint.
13
- *
14
- * Both categories are informational at the wave-completeness gate — they do
15
- * not fail closure by themselves. The gate continues to enforce that every
16
- * manifest Story is closed; recuts and parked follow-ons are additional
17
- * transparency.
18
- */
19
-
20
- import { parseRecutMarker } from './recut.js';
21
-
22
- /**
23
- * Partition Stories under an Epic into manifest, recut, and parked buckets.
24
- *
25
- * @param {number[]} manifestStoryIds IDs present in the dispatch manifest.
26
- * @param {Array<{ id: number, title?: string, body?: string, state?: string, labels?: string[] }>} storiesUnderEpic
27
- * All `type::story` tickets under the Epic.
28
- * @returns {{
29
- * manifest: Array<object>,
30
- * recuts: Array<{ storyId: number, parentId: number, title: string, state: string }>,
31
- * parked: Array<{ storyId: number, title: string, state: string }>,
32
- * }}
33
- */
34
- export function classifyStoriesAgainstManifest(
35
- manifestStoryIds,
36
- storiesUnderEpic,
37
- ) {
38
- const manifestSet = new Set(manifestStoryIds.map(Number));
39
- const manifest = [];
40
- const recuts = [];
41
- const parked = [];
42
-
43
- for (const story of storiesUnderEpic) {
44
- if (manifestSet.has(story.id)) {
45
- manifest.push(story);
46
- continue;
47
- }
48
- const marker = parseRecutMarker(story.body);
49
- if (marker && manifestSet.has(marker.parentStoryId)) {
50
- recuts.push({
51
- storyId: story.id,
52
- parentId: marker.parentStoryId,
53
- title: story.title ?? '',
54
- state: story.state ?? 'open',
55
- });
56
- } else {
57
- parked.push({
58
- storyId: story.id,
59
- title: story.title ?? '',
60
- state: story.state ?? 'open',
61
- });
62
- }
63
- }
64
-
65
- return { manifest, recuts, parked };
66
- }
67
-
68
- /**
69
- * Render the structured `parked-follow-ons` comment body for an Epic.
70
- * Idempotent: the same input produces identical output, so repeated upserts
71
- * don't churn comment revisions.
72
- *
73
- * @param {number} epicId
74
- * @param {ReturnType<typeof classifyStoriesAgainstManifest>} classification
75
- * @returns {string}
76
- */
77
- export function renderParkedFollowOnsComment(epicId, classification) {
78
- const { recuts, parked } = classification;
79
-
80
- const lines = [
81
- `## 🪝 Parked Follow-Ons & Recuts — Epic #${epicId}`,
82
- '',
83
- 'Stories created under this Epic that are **not** in the frozen dispatch',
84
- 'manifest. Surfaced here so `/deliver` can gate on them at the',
85
- 'completeness check.',
86
- '',
87
- `- **Recuts** (attributable to a manifest Story): ${recuts.length}`,
88
- `- **Parked follow-ons** (no manifest lineage): ${parked.length}`,
89
- '',
90
- ];
91
-
92
- if (recuts.length > 0) {
93
- lines.push('### Recuts');
94
- lines.push('');
95
- lines.push('| Story | Recut-of | State | Title |');
96
- lines.push('| :--- | :--- | :--- | :--- |');
97
- for (const r of recuts) {
98
- lines.push(
99
- `| #${r.storyId} | #${r.parentId} | ${r.state} | ${r.title} |`,
100
- );
101
- }
102
- lines.push('');
103
- }
104
-
105
- if (parked.length > 0) {
106
- lines.push('### Parked Follow-Ons');
107
- lines.push('');
108
- lines.push('| Story | State | Title |');
109
- lines.push('| :--- | :--- | :--- |');
110
- for (const p of parked) {
111
- lines.push(`| #${p.storyId} | ${p.state} | ${p.title} |`);
112
- }
113
- lines.push('');
114
- lines.push(
115
- '> **Action required**: adopt each Story into the current Epic (by',
116
- '> re-running the dispatcher so the manifest is refreshed), or explicitly',
117
- '> defer by closing the Story with `state_reason=not_planned`.',
118
- );
119
- lines.push('');
120
- }
121
-
122
- if (recuts.length === 0 && parked.length === 0) {
123
- lines.push(
124
- '✅ No out-of-manifest Stories detected — every Story under this Epic is in the dispatch manifest.',
125
- );
126
- lines.push('');
127
- }
128
-
129
- lines.push('```json');
130
- lines.push(
131
- JSON.stringify(
132
- {
133
- recuts: recuts.map((r) => ({
134
- storyId: r.storyId,
135
- parentId: r.parentId,
136
- state: r.state,
137
- })),
138
- parked: parked.map((p) => ({ storyId: p.storyId, state: p.state })),
139
- },
140
- null,
141
- 2,
142
- ),
143
- );
144
- lines.push('```');
145
-
146
- return lines.join('\n');
147
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * phase-runner.js — Lightweight wrappers for "named phase" error handling.
3
- *
4
- * Many orchestration entry points (`single-story-init.js`,
5
- * `single-story-close.js`, and other phase modules) repeat the same
6
- * `try { ... } catch (err) {
7
- * logger.error('[phase=foo] ' + err.message); ... }` block. These two
8
- * helpers consolidate that pattern.
9
- *
10
- * - `runPhase(name, fn, opts)`: Run `fn()`. On error, log with a
11
- * `[phase=<name>]` prefix. If `fatal: true`, rethrow; otherwise
12
- * return `opts.fallback` (default `undefined`) so the caller can
13
- * continue.
14
- *
15
- * - `runSafely(fn, opts)`: Run `fn()` and swallow any error after
16
- * logging it. Convenience for unnamed best-effort cleanup steps.
17
- *
18
- * Both helpers handle sync and async `fn`s — the return value is always
19
- * a Promise that resolves to the function's value (or `fallback`).
20
- */
21
-
22
- import { NOOP_LOGGER } from '../Logger.js';
23
-
24
- /**
25
- * @typedef {object} PhaseLogger
26
- * @property {(msg: string) => void} [error]
27
- * @property {(msg: string) => void} [warn]
28
- */
29
-
30
- function pickLogger(logger) {
31
- if (!logger) return NOOP_LOGGER;
32
- return {
33
- error:
34
- typeof logger.error === 'function'
35
- ? logger.error.bind(logger)
36
- : NOOP_LOGGER.error,
37
- warn:
38
- typeof logger.warn === 'function'
39
- ? logger.warn.bind(logger)
40
- : NOOP_LOGGER.warn,
41
- };
42
- }
43
-
44
- /**
45
- * Run `fn` under a named phase. On error:
46
- * - logs `[phase=<name>] <message>` via `logger.error`
47
- * - if `fatal` is true, rethrows the original error
48
- * - otherwise returns `fallback`
49
- *
50
- * @template T
51
- * @param {string} name - Phase name (used in the log prefix).
52
- * @param {() => (T | Promise<T>)} fn
53
- * @param {{ logger?: PhaseLogger, fallback?: T, fatal?: boolean }} [opts]
54
- * @returns {Promise<T | undefined>}
55
- */
56
- export async function runPhase(name, fn, opts = {}) {
57
- const logger = pickLogger(opts.logger);
58
- const fatal = opts.fatal === true;
59
- try {
60
- return await fn();
61
- } catch (err) {
62
- const message = err instanceof Error ? err.message : String(err);
63
- logger.error(`[phase=${name}] ${message}`);
64
- if (fatal) throw err;
65
- return opts.fallback;
66
- }
67
- }
68
-
69
- /**
70
- * Run `fn` and swallow errors after logging. Equivalent to
71
- * `runPhase('safe', fn, { logger, fatal: false })` but without requiring
72
- * the caller to invent a phase name.
73
- *
74
- * @template T
75
- * @param {() => (T | Promise<T>)} fn
76
- * @param {{ logger?: PhaseLogger }} [opts]
77
- * @returns {Promise<T | undefined>}
78
- */
79
- export async function runSafely(fn, opts = {}) {
80
- const logger = pickLogger(opts.logger);
81
- try {
82
- return await fn();
83
- } catch (err) {
84
- const message = err instanceof Error ? err.message : String(err);
85
- logger.warn(`[phase=safe] ${message}`);
86
- return undefined;
87
- }
88
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Recut markers track when a Story is carved off mid-sprint from another
3
- * Story that is already in the dispatch manifest. The marker is an HTML
4
- * comment embedded in the new Story's body:
5
- *
6
- * <!-- recut-of: #641 -->
7
- *
8
- * Downstream consumers (the wave-completeness gate, the sprint retro)
9
- * read the marker to attribute the new Story back to its ancestor so
10
- * counts line up with the frozen manifest.
11
- */
12
-
13
- const MARKER_RE = /<!--\s*recut-of:\s*#?(\d+)\s*-->/i;
14
-
15
- /**
16
- * Parse a recut marker out of a Story body.
17
- *
18
- * @param {string | null | undefined} body
19
- * @returns {{ parentStoryId: number, raw: string } | null}
20
- */
21
- export function parseRecutMarker(body) {
22
- if (typeof body !== 'string') return null;
23
- const match = body.match(MARKER_RE);
24
- if (!match) return null;
25
- return { parentStoryId: Number.parseInt(match[1], 10), raw: match[0] };
26
- }
27
-
28
- /**
29
- * Format a recut marker for a given parent Story ID.
30
- *
31
- * @param {number} parentStoryId
32
- * @returns {string}
33
- */
34
- export function formatRecutMarker(parentStoryId) {
35
- return `<!-- recut-of: #${parentStoryId} -->`;
36
- }
37
-
38
- /**
39
- * Return a body with the recut marker present. Replaces any existing
40
- * marker so the parent reference is always up to date; appends a fresh
41
- * marker when none is present.
42
- *
43
- * @param {string | null | undefined} body
44
- * @param {number} parentStoryId
45
- * @returns {string}
46
- */
47
- export function injectRecutMarker(body, parentStoryId) {
48
- const marker = formatRecutMarker(parentStoryId);
49
- const existing = parseRecutMarker(body);
50
- if (existing) {
51
- if (existing.parentStoryId === parentStoryId) return body ?? '';
52
- return (body ?? '').replace(MARKER_RE, marker);
53
- }
54
- const trimmed = (body ?? '').replace(/\s+$/, '');
55
- return trimmed.length > 0 ? `${trimmed}\n\n${marker}\n` : `${marker}\n`;
56
- }