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.
- package/.agents/docs/configuration.md +25 -0
- package/.agents/rules/testing-standards.md +56 -0
- 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-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/drain-pending-cleanup.js +18 -1
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/install-ledger.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/loop-health.js +1 -1
- package/.agents/scripts/lib/cli-args.js +0 -2
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +46 -6
- 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 +1 -1
- 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 +1 -1
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
- 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/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/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- 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/ticketing/reads.js +3 -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/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/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/transpile.js +16 -3
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/tickets.js +109 -16
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +0 -2
- package/.agents/scripts/single-story-init.js +5 -1
- package/docs/CHANGELOG.md +32 -0
- 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.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
|
@@ -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
|
-
}
|