mandrel 1.57.0 → 1.59.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 +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +241 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +16 -3
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +21 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import { spawnSync } from 'node:child_process';
|
|
35
|
+
import { parseProviderFindings } from './parse-findings.js';
|
|
35
36
|
import { renderDepthDirective } from './review-depth.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -138,65 +139,12 @@ export function mapSecurityReviewSeverity(raw) {
|
|
|
138
139
|
* @throws {Error} when stdout is not parseable JSON.
|
|
139
140
|
*/
|
|
140
141
|
export function parseSecurityReviewFindings(rawStdout) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch (err) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`[security-review] Failed to parse /security-review stdout as JSON: ${
|
|
150
|
-
err?.message ?? err
|
|
151
|
-
}`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
156
|
-
if (Array.isArray(parsed.findings)) parsed = parsed.findings;
|
|
157
|
-
else if (parsed.result !== undefined) parsed = parsed.result;
|
|
158
|
-
else if (parsed.data !== undefined) parsed = parsed.data;
|
|
159
|
-
}
|
|
160
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
161
|
-
if (Array.isArray(parsed.findings)) parsed = parsed.findings;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!Array.isArray(parsed)) return [];
|
|
165
|
-
|
|
166
|
-
/** @type {Finding[]} */
|
|
167
|
-
const findings = [];
|
|
168
|
-
for (const entry of parsed) {
|
|
169
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
170
|
-
const title =
|
|
171
|
-
typeof entry.title === 'string' && entry.title.trim().length > 0
|
|
172
|
-
? entry.title.trim()
|
|
173
|
-
: null;
|
|
174
|
-
const body =
|
|
175
|
-
typeof entry.body === 'string' && entry.body.trim().length > 0
|
|
176
|
-
? entry.body
|
|
177
|
-
: typeof entry.message === 'string' && entry.message.trim().length > 0
|
|
178
|
-
? entry.message
|
|
179
|
-
: null;
|
|
180
|
-
if (!title || !body) continue;
|
|
181
|
-
/** @type {Finding} */
|
|
182
|
-
const finding = {
|
|
183
|
-
severity: mapSecurityReviewSeverity(entry.severity),
|
|
184
|
-
title,
|
|
185
|
-
body,
|
|
186
|
-
category:
|
|
187
|
-
typeof entry.category === 'string' && entry.category.length > 0
|
|
188
|
-
? entry.category
|
|
189
|
-
: 'security',
|
|
190
|
-
};
|
|
191
|
-
if (typeof entry.file === 'string' && entry.file.length > 0) {
|
|
192
|
-
finding.file = entry.file;
|
|
193
|
-
}
|
|
194
|
-
if (Number.isInteger(entry.line) && entry.line > 0) {
|
|
195
|
-
finding.line = entry.line;
|
|
196
|
-
}
|
|
197
|
-
findings.push(finding);
|
|
198
|
-
}
|
|
199
|
-
return findings;
|
|
142
|
+
return parseProviderFindings(rawStdout, {
|
|
143
|
+
errorPrefix:
|
|
144
|
+
'[security-review] Failed to parse /security-review stdout as JSON',
|
|
145
|
+
mapSeverity: mapSecurityReviewSeverity,
|
|
146
|
+
defaultCategory: 'security',
|
|
147
|
+
});
|
|
200
148
|
}
|
|
201
149
|
|
|
202
150
|
/**
|
|
@@ -22,10 +22,8 @@
|
|
|
22
22
|
* that mock the upstream module URLs.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
runCloseValidation as defaultRunCloseValidation,
|
|
28
|
-
} from '../../../close-validation.js';
|
|
25
|
+
import { buildDefaultGates as defaultBuildDefaultGates } from '../../../close-validation/gates.js';
|
|
26
|
+
import { runCloseValidation as defaultRunCloseValidation } from '../../../close-validation/runner.js';
|
|
29
27
|
import { Logger } from '../../../Logger.js';
|
|
30
28
|
|
|
31
29
|
/**
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* normalize-pr-title.js — guarantee the standalone-Story PR title is a
|
|
3
|
+
* valid Conventional Commit subject so the squash-merge subject on `main`
|
|
4
|
+
* parses for release-please.
|
|
5
|
+
*
|
|
6
|
+
* Story #3969 (framework gap). The repo squash-merges, and GitHub uses the
|
|
7
|
+
* PR title as the squash-commit subject. `buildPullRequest` previously
|
|
8
|
+
* emitted the raw human issue title (`<storyTitle> (#<id>)`), which is a
|
|
9
|
+
* plain description ("Rename the published npm package…") that
|
|
10
|
+
* release-please's Conventional-Commit parser rejects:
|
|
11
|
+
*
|
|
12
|
+
* ❯ commit could not be parsed: … Rename the published npm package …
|
|
13
|
+
* ❯ error: unexpected token ' ' at 1:7, valid tokens [(, !, :]
|
|
14
|
+
* ❯ commits: 0 → no release cut
|
|
15
|
+
*
|
|
16
|
+
* The `commit-msg` commitlint Husky hook only validates *local* commits and
|
|
17
|
+
* never runs on a GitHub-UI squash-merge title, so nothing mechanized the
|
|
18
|
+
* documented "author the PR title in conventional form" contract. This
|
|
19
|
+
* module mechanizes it.
|
|
20
|
+
*
|
|
21
|
+
* Contract (pure where possible — the only side effect is an injectable
|
|
22
|
+
* `git log` read used to derive the type):
|
|
23
|
+
*
|
|
24
|
+
* - If `storyTitle` is **already** a parseable Conventional Commit
|
|
25
|
+
* subject, it is preserved verbatim and suffixed with `(#<storyId>)`.
|
|
26
|
+
* No re-prefixing, no double type.
|
|
27
|
+
* - Otherwise the title is **synthesized** into conventional form:
|
|
28
|
+
* `<type>: <descriptive text> (#<storyId>)`. The `type` is derived
|
|
29
|
+
* from the branch's own (already-conventional) commit subjects when
|
|
30
|
+
* available, falling back to a safe configured default (`chore`).
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the already-normalized Epic-finalize default in
|
|
33
|
+
* `lib/orchestration/finalize/open-or-locate-pr.js` (`feat: Epic #<id>`),
|
|
34
|
+
* bringing the standalone path to the same guarantee.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { gitSpawn as defaultGitSpawn } from '../../../git-utils.js';
|
|
38
|
+
import { Logger as DefaultLogger } from '../../../Logger.js';
|
|
39
|
+
|
|
40
|
+
/** Safe default Conventional-Commit type when none can be derived. */
|
|
41
|
+
export const DEFAULT_CONVENTIONAL_TYPE = 'chore';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The Conventional-Commit types Mandrel accepts. Mirrors
|
|
45
|
+
* `commitlint.config.js` → `type-enum` and `release-please-config.json` →
|
|
46
|
+
* `changelog-sections`. Kept in sync by hand (single hard-cutover, no
|
|
47
|
+
* shim) — adding a type means touching all three.
|
|
48
|
+
*/
|
|
49
|
+
export const CONVENTIONAL_TYPES = Object.freeze([
|
|
50
|
+
'feat',
|
|
51
|
+
'fix',
|
|
52
|
+
'perf',
|
|
53
|
+
'refactor',
|
|
54
|
+
'revert',
|
|
55
|
+
'docs',
|
|
56
|
+
'style',
|
|
57
|
+
'chore',
|
|
58
|
+
'test',
|
|
59
|
+
'build',
|
|
60
|
+
'ci',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// Precedence used when a branch carries a mix of conventional types: pick
|
|
64
|
+
// the most release-significant one so the squash subject communicates the
|
|
65
|
+
// branch's headline impact (and release-please bumps appropriately).
|
|
66
|
+
const TYPE_PRECEDENCE = Object.freeze([
|
|
67
|
+
'feat',
|
|
68
|
+
'fix',
|
|
69
|
+
'perf',
|
|
70
|
+
'refactor',
|
|
71
|
+
'revert',
|
|
72
|
+
'docs',
|
|
73
|
+
'style',
|
|
74
|
+
'test',
|
|
75
|
+
'build',
|
|
76
|
+
'ci',
|
|
77
|
+
'chore',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const TYPE_GROUP = CONVENTIONAL_TYPES.join('|');
|
|
81
|
+
// Anchored Conventional-Commit header matcher:
|
|
82
|
+
// <type>(<optional scope>)<optional !>: <non-empty description>
|
|
83
|
+
// Mirrors the shape `@commitlint/config-conventional` enforces (a known
|
|
84
|
+
// type, an optional parenthesised scope, an optional breaking `!`, a
|
|
85
|
+
// colon-space separator, and a non-empty subject). Used for the pure
|
|
86
|
+
// "is this already conventional?" check and to pull the type off a branch
|
|
87
|
+
// commit subject without spawning commitlint per call.
|
|
88
|
+
const CONVENTIONAL_HEADER_RE = new RegExp(
|
|
89
|
+
`^(?:${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?: \\S.*$`,
|
|
90
|
+
);
|
|
91
|
+
const LEADING_TYPE_RE = new RegExp(
|
|
92
|
+
`^(${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?:`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* True iff `subject` is a parseable Conventional Commit subject under the
|
|
97
|
+
* repo's type vocabulary. Pure.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} subject
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function isConventionalSubject(subject) {
|
|
103
|
+
if (typeof subject !== 'string') return false;
|
|
104
|
+
return CONVENTIONAL_HEADER_RE.test(subject.trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract the Conventional-Commit `type` from a single commit subject, or
|
|
109
|
+
* `null` when the subject is not conventional. Pure.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} subject
|
|
112
|
+
* @returns {string|null}
|
|
113
|
+
*/
|
|
114
|
+
export function parseConventionalType(subject) {
|
|
115
|
+
if (typeof subject !== 'string') return null;
|
|
116
|
+
const match = subject.trim().match(LEADING_TYPE_RE);
|
|
117
|
+
return match ? match[1] : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pick the most release-significant type from a list of conventional
|
|
122
|
+
* types, honouring `TYPE_PRECEDENCE`. Returns `null` for an empty list.
|
|
123
|
+
* Pure.
|
|
124
|
+
*
|
|
125
|
+
* @param {string[]} types
|
|
126
|
+
* @returns {string|null}
|
|
127
|
+
*/
|
|
128
|
+
export function pickDominantType(types) {
|
|
129
|
+
const present = new Set(types.filter(Boolean));
|
|
130
|
+
for (const candidate of TYPE_PRECEDENCE) {
|
|
131
|
+
if (present.has(candidate)) return candidate;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read the branch's own commit subjects (commits unique to the Story
|
|
138
|
+
* branch relative to the base branch) and derive the dominant
|
|
139
|
+
* Conventional-Commit type. Returns `DEFAULT_CONVENTIONAL_TYPE` when no
|
|
140
|
+
* conventional subject is found or the git read fails.
|
|
141
|
+
*
|
|
142
|
+
* @param {{
|
|
143
|
+
* storyBranch: string,
|
|
144
|
+
* baseBranch: string,
|
|
145
|
+
* cwd?: string,
|
|
146
|
+
* gitSpawn?: typeof defaultGitSpawn,
|
|
147
|
+
* logger?: { warn?: Function },
|
|
148
|
+
* }} args
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
export function deriveTypeFromBranchCommits({
|
|
152
|
+
storyBranch,
|
|
153
|
+
baseBranch,
|
|
154
|
+
cwd = process.cwd(),
|
|
155
|
+
gitSpawn = defaultGitSpawn,
|
|
156
|
+
logger = DefaultLogger,
|
|
157
|
+
}) {
|
|
158
|
+
try {
|
|
159
|
+
const range = `${baseBranch}..${storyBranch}`;
|
|
160
|
+
const result = gitSpawn(cwd, 'log', '--no-merges', '--format=%s', range);
|
|
161
|
+
if (!result || result.status !== 0) {
|
|
162
|
+
logger?.warn?.(
|
|
163
|
+
`[normalize-pr-title] git log ${range} failed (status=${result?.status ?? 'n/a'}); ` +
|
|
164
|
+
`defaulting type to "${DEFAULT_CONVENTIONAL_TYPE}".`,
|
|
165
|
+
);
|
|
166
|
+
return DEFAULT_CONVENTIONAL_TYPE;
|
|
167
|
+
}
|
|
168
|
+
const types = String(result.stdout ?? '')
|
|
169
|
+
.split('\n')
|
|
170
|
+
.map((line) => parseConventionalType(line))
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
return pickDominantType(types) ?? DEFAULT_CONVENTIONAL_TYPE;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger?.warn?.(
|
|
175
|
+
`[normalize-pr-title] could not derive type from branch commits ` +
|
|
176
|
+
`(defaulting to "${DEFAULT_CONVENTIONAL_TYPE}"): ${err?.message ?? err}`,
|
|
177
|
+
);
|
|
178
|
+
return DEFAULT_CONVENTIONAL_TYPE;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Produce a PR title that parses as a Conventional Commit.
|
|
184
|
+
*
|
|
185
|
+
* - Already-conventional `storyTitle` → preserved verbatim + `(#<id>)`.
|
|
186
|
+
* - Otherwise → `<derivedType>: <storyTitle> (#<id>)`.
|
|
187
|
+
* - Empty / missing `storyTitle` → `<derivedType>: Story #<id>`.
|
|
188
|
+
*
|
|
189
|
+
* The type derivation (`deriveTypeFromBranchCommits`) is the only side
|
|
190
|
+
* effect, and is skipped entirely when the title is already conventional.
|
|
191
|
+
*
|
|
192
|
+
* @param {{
|
|
193
|
+
* storyTitle: string,
|
|
194
|
+
* storyId: number|string,
|
|
195
|
+
* storyBranch?: string,
|
|
196
|
+
* baseBranch?: string,
|
|
197
|
+
* cwd?: string,
|
|
198
|
+
* gitSpawn?: typeof defaultGitSpawn,
|
|
199
|
+
* logger?: { warn?: Function },
|
|
200
|
+
* }} args
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
export function normalizePrTitle({
|
|
204
|
+
storyTitle,
|
|
205
|
+
storyId,
|
|
206
|
+
storyBranch,
|
|
207
|
+
baseBranch,
|
|
208
|
+
cwd = process.cwd(),
|
|
209
|
+
gitSpawn = defaultGitSpawn,
|
|
210
|
+
logger = DefaultLogger,
|
|
211
|
+
}) {
|
|
212
|
+
const idSuffix = `(#${storyId})`;
|
|
213
|
+
const trimmed = typeof storyTitle === 'string' ? storyTitle.trim() : '';
|
|
214
|
+
|
|
215
|
+
// Already conventional → preserve verbatim, append the id reference.
|
|
216
|
+
if (isConventionalSubject(trimmed)) {
|
|
217
|
+
return `${trimmed} ${idSuffix}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Not conventional → derive a type and synthesize.
|
|
221
|
+
const type =
|
|
222
|
+
storyBranch && baseBranch
|
|
223
|
+
? deriveTypeFromBranchCommits({
|
|
224
|
+
storyBranch,
|
|
225
|
+
baseBranch,
|
|
226
|
+
cwd,
|
|
227
|
+
gitSpawn,
|
|
228
|
+
logger,
|
|
229
|
+
})
|
|
230
|
+
: DEFAULT_CONVENTIONAL_TYPE;
|
|
231
|
+
|
|
232
|
+
// Lowercase the leading character of a synthesized description so the
|
|
233
|
+
// subject satisfies commitlint's `subject-case` rule (matching the
|
|
234
|
+
// `shapeMergeSubject` behaviour). An already-conventional title is left
|
|
235
|
+
// untouched (it was preserved verbatim above). The empty-title fallback
|
|
236
|
+
// uses a lowercased `story #<id>` for the same reason.
|
|
237
|
+
const rawDescription = trimmed.length > 0 ? trimmed : `Story #${storyId}`;
|
|
238
|
+
const description =
|
|
239
|
+
rawDescription.charAt(0).toLowerCase() + rawDescription.slice(1);
|
|
240
|
+
return `${type}: ${description} ${idSuffix}`;
|
|
241
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { parseSprintArgs } from '../../../cli-args.js';
|
|
13
|
-
import { PROJECT_ROOT } from '../../../
|
|
13
|
+
import { PROJECT_ROOT } from '../../../project-root.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Resolve a flag value from an explicit override, a parsed CLI arg, or a
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { gh as defaultGh } from '../../../gh-exec.js';
|
|
21
21
|
import { Logger } from '../../../Logger.js';
|
|
22
|
+
import { normalizePrTitle } from './normalize-pr-title.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Probe for an existing open PR with `head = storyBranch`; create one if
|
|
@@ -76,9 +77,21 @@ export async function ensurePullRequestWith({
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
progress('PR', `Opening PR for ${storyBranch} → ${baseBranch}...`);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// The repo squash-merges and GitHub uses the PR title as the squash
|
|
81
|
+
// subject on `main`. A raw human issue title is not a Conventional
|
|
82
|
+
// Commit, so release-please silently counts it as 0 releasable commits
|
|
83
|
+
// (Story #3969). Normalize the title to conventional form: preserve an
|
|
84
|
+
// already-conventional `storyTitle` verbatim, otherwise synthesize a
|
|
85
|
+
// type derived from the branch's own commit subjects (default `chore`).
|
|
86
|
+
// `gh-exec` spawns `gh` against the current process cwd (the worktree),
|
|
87
|
+
// so the branch-commit read uses the same cwd.
|
|
88
|
+
const title = normalizePrTitle({
|
|
89
|
+
storyTitle,
|
|
90
|
+
storyId,
|
|
91
|
+
storyBranch,
|
|
92
|
+
baseBranch,
|
|
93
|
+
cwd: _cwd ?? process.cwd(),
|
|
94
|
+
});
|
|
82
95
|
const body = [
|
|
83
96
|
`Closes #${storyId}`,
|
|
84
97
|
'',
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import nodeFs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
runCloseValidation,
|
|
6
|
-
} from '../../close-validation.js';
|
|
3
|
+
import { buildDefaultGates } from '../../close-validation/gates.js';
|
|
4
|
+
import { runCloseValidation } from '../../close-validation/runner.js';
|
|
7
5
|
import { resolveConfig } from '../../config-resolver.js';
|
|
8
6
|
import { getStoryBranch, gitSync } from '../../git-utils.js';
|
|
9
7
|
import { Logger } from '../../Logger.js';
|
|
@@ -39,16 +39,20 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} from './ticket-lease.js';
|
|
42
|
+
acquireLeaseFailClosed,
|
|
43
|
+
resolveOperatorFromCandidates,
|
|
44
|
+
} from './lease-guard-shared.js';
|
|
45
|
+
import { releaseLease } from './ticket-lease.js';
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Resolve the operator handle used as the lease owner from resolved config.
|
|
49
|
-
* Routes through the shared
|
|
49
|
+
* Routes through the shared lease-guard kernel
|
|
50
|
+
* (`lease-guard-shared.resolveOperatorFromCandidates`) so a leading `@` is
|
|
50
51
|
* stripped (the assignees API expects bare logins, not `@`-prefixed mentions)
|
|
51
|
-
* and the self-held-claim comparison matches.
|
|
52
|
+
* and the self-held-claim comparison matches. The standalone surface's
|
|
53
|
+
* missing-handle policy is `'throw'` (intentional divergence from the plan
|
|
54
|
+
* path's `'null'`): init has no best-effort leg that can degrade, so an
|
|
55
|
+
* unowned lease must refuse immediately.
|
|
52
56
|
*
|
|
53
57
|
* @param {object} config Resolved `.agentrc.json` config.
|
|
54
58
|
* @returns {string} Bare operator handle.
|
|
@@ -58,17 +62,16 @@ import {
|
|
|
58
62
|
* path cannot safely serialise concurrent runs.
|
|
59
63
|
*/
|
|
60
64
|
export function resolveOperator(config) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw
|
|
65
|
+
return resolveOperatorFromCandidates({
|
|
66
|
+
candidates: [config?.github?.operatorHandle],
|
|
67
|
+
missingHandleBehavior: 'throw',
|
|
68
|
+
missingHandleMessage:
|
|
64
69
|
'single-story lease: no operator identity is configured. ' +
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
return handle;
|
|
70
|
+
'github.operatorHandle is unset or still the shipped `@[USERNAME]` ' +
|
|
71
|
+
'placeholder, so the standalone Story lease has no owner. Set your own ' +
|
|
72
|
+
'handle in .agentrc.local.json (e.g. { "github": { "operatorHandle": ' +
|
|
73
|
+
'"@your-login" } }) and re-run.',
|
|
74
|
+
});
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
@@ -99,31 +102,25 @@ export async function acquireStoryLease({
|
|
|
99
102
|
now,
|
|
100
103
|
}) {
|
|
101
104
|
const owner = operator ?? resolveOperator(config);
|
|
102
|
-
// Fail closed: with no live-heartbeat source on the standalone path,
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
|
|
108
|
-
const result = await acquireLease({
|
|
105
|
+
// Fail closed: with no live-heartbeat source on the standalone path, the
|
|
106
|
+
// shared kernel anchors `heartbeatAt` to the same `now` the primitive
|
|
107
|
+
// evaluates against, so `isClaimLive` returns true for any foreign owner
|
|
108
|
+
// and `acquireLease` refuses unless `steal` is set.
|
|
109
|
+
return acquireLeaseFailClosed({
|
|
109
110
|
provider,
|
|
110
111
|
ticketId: storyId,
|
|
111
112
|
operator: owner,
|
|
112
|
-
heartbeatAt: resolvedNow,
|
|
113
113
|
steal,
|
|
114
114
|
config,
|
|
115
|
-
now
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
throw new Error(
|
|
115
|
+
now,
|
|
116
|
+
anchorHeartbeatToNow: true,
|
|
117
|
+
renderRefusal: (result) =>
|
|
119
118
|
`single-story lease: Story #${storyId} is currently held by @${result.owner}. ` +
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
119
|
+
'Another /single-story-deliver run owns this Story. Coordinate with that ' +
|
|
120
|
+
'operator, or re-run with --steal to forcibly transfer the claim once you ' +
|
|
121
|
+
'have confirmed the other run is dead. (The standalone path has no Epic ' +
|
|
122
|
+
'heartbeat ledger, so a foreign assignee always blocks unless stolen.)',
|
|
123
|
+
});
|
|
127
124
|
}
|
|
128
125
|
|
|
129
126
|
/**
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
|
|
16
|
-
import { PROJECT_ROOT } from '../config-resolver.js';
|
|
17
15
|
import { Logger } from '../Logger.js';
|
|
16
|
+
import { PROJECT_ROOT } from '../project-root.js';
|
|
18
17
|
|
|
19
18
|
const POLICY_HEADING_RE = /^## Policy Capsule\s*$/;
|
|
20
19
|
const ANY_H2_RE = /^## /;
|