mandrel 1.88.0 → 1.90.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 +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +62 -27
- package/.agents/docs/configuration.md +5 -4
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +10 -6
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +39 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/coverage-capture.js +17 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/close-validation/gates.js +64 -24
- package/.agents/scripts/lib/config/ci.js +12 -1
- package/.agents/scripts/lib/config/runners.js +13 -5
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -8
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +47 -15
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/npm-scripts.js +55 -0
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +193 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +248 -13
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +109 -12
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +46 -4
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +246 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +8 -1
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +116 -43
- package/.agents/workflows/helpers/deliver-epic.md +123 -54
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -366
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -224
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -15,6 +15,7 @@ import { runAutoMergePhase } from './phases/auto-merge.js';
|
|
|
15
15
|
import { runBaseSyncPhase } from './phases/base-sync.js';
|
|
16
16
|
import { runCloseValidationPhase } from './phases/close-validation.js';
|
|
17
17
|
import { parsePrNumber, runStoryScopeReview } from './phases/code-review.js';
|
|
18
|
+
import { runConfirmMergePhase } from './phases/confirm-merge.js';
|
|
18
19
|
import { parseCloseOptions } from './phases/options.js';
|
|
19
20
|
import { ensurePullRequestWith } from './phases/pull-request.js';
|
|
20
21
|
import { pushStoryBranch } from './phases/push.js';
|
|
@@ -57,6 +58,7 @@ async function runPrePushPhases({
|
|
|
57
58
|
await runWrongTreeGuardPhase({
|
|
58
59
|
cwd,
|
|
59
60
|
worktreePath,
|
|
61
|
+
baseBranch,
|
|
60
62
|
storyId,
|
|
61
63
|
provider,
|
|
62
64
|
progress,
|
|
@@ -204,6 +206,8 @@ function closeResult({
|
|
|
204
206
|
autoMergeReason,
|
|
205
207
|
worktreeReaped,
|
|
206
208
|
leaseReleased,
|
|
209
|
+
waitedForMerge = false,
|
|
210
|
+
merged = false,
|
|
207
211
|
}) {
|
|
208
212
|
return {
|
|
209
213
|
storyId,
|
|
@@ -217,9 +221,13 @@ function closeResult({
|
|
|
217
221
|
autoMergeReason,
|
|
218
222
|
worktreeReaped,
|
|
219
223
|
leaseReleased,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
waitedForMerge,
|
|
225
|
+
merged,
|
|
226
|
+
note: waitedForMerge
|
|
227
|
+
? 'Headless must-land: PR merge confirmed. Story flipped agent::closing → agent::done and the issue closed (confirmStoryMerged).'
|
|
228
|
+
: autoMergeEnabled
|
|
229
|
+
? 'PR open against baseBranch with auto-merge enabled. Story rests at agent::closing (issue stays OPEN). GitHub will squash-merge when required checks pass; run single-story-confirm-merge.js after the merge confirms to flip agent::done and close the issue (the Closes #<id> footer also auto-closes it).'
|
|
230
|
+
: 'PR open against baseBranch. Story rests at agent::closing (issue stays OPEN). Operator merges via GitHub UI; run single-story-confirm-merge.js after the merge confirms to flip agent::done (the Closes #<id> footer also auto-closes the issue).',
|
|
223
231
|
};
|
|
224
232
|
}
|
|
225
233
|
|
|
@@ -230,6 +238,8 @@ export async function runSingleStoryClose({
|
|
|
230
238
|
skipSync: skipSyncParam,
|
|
231
239
|
noAutoMerge: noAutoMergeParam,
|
|
232
240
|
noFullScopeCrap: noFullScopeCrapParam,
|
|
241
|
+
waitForMerge: waitForMergeParam,
|
|
242
|
+
noWaitForMerge: noWaitForMergeParam,
|
|
233
243
|
injectedProvider,
|
|
234
244
|
injectedConfig,
|
|
235
245
|
injectedNotify,
|
|
@@ -246,10 +256,12 @@ export async function runSingleStoryClose({
|
|
|
246
256
|
skipSyncParam,
|
|
247
257
|
noAutoMergeParam,
|
|
248
258
|
noFullScopeCrapParam,
|
|
259
|
+
waitForMergeParam,
|
|
260
|
+
noWaitForMergeParam,
|
|
249
261
|
});
|
|
250
262
|
if (!options.storyId) {
|
|
251
263
|
throw new Error(
|
|
252
|
-
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--no-full-scope-crap]',
|
|
264
|
+
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--no-full-scope-crap] [--wait-merge|--no-wait-merge]',
|
|
253
265
|
);
|
|
254
266
|
}
|
|
255
267
|
|
|
@@ -341,6 +353,56 @@ export async function runSingleStoryClose({
|
|
|
341
353
|
WorktreeManager,
|
|
342
354
|
});
|
|
343
355
|
const leaseReleased = await releaseLease(leaseArgs);
|
|
356
|
+
|
|
357
|
+
// Story #4428 — headless must-land: `--wait-merge` polls the just-armed
|
|
358
|
+
// PR to merge confirmation (or an explicit `agent::blocked` +
|
|
359
|
+
// `merge.unlanded`) instead of resting at `agent::closing`. Attended
|
|
360
|
+
// (non-headless) runs never set this flag, so the exit shape below is
|
|
361
|
+
// unreachable and the pre-existing early-return is byte-identical.
|
|
362
|
+
if (options.waitForMerge) {
|
|
363
|
+
const waitOutcome = await runConfirmMergePhase({
|
|
364
|
+
cwd: options.cwd,
|
|
365
|
+
storyId: options.storyId,
|
|
366
|
+
prNumber,
|
|
367
|
+
prUrl,
|
|
368
|
+
autoMergeEnabled,
|
|
369
|
+
autoMergeReason,
|
|
370
|
+
provider,
|
|
371
|
+
config,
|
|
372
|
+
progress,
|
|
373
|
+
injectedGh,
|
|
374
|
+
injectedNotify,
|
|
375
|
+
});
|
|
376
|
+
if (!waitOutcome.confirmed) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`[single-story-close] Headless must-land: PR ${prUrl} did not reach a confirmed merge ` +
|
|
379
|
+
`(blockClass=${waitOutcome.blockClass}). Story #${options.storyId} was transitioned to ` +
|
|
380
|
+
`agent::blocked with a merge.unlanded lifecycle event. Reason: ${waitOutcome.reason}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const result = closeResult({
|
|
384
|
+
storyId: options.storyId,
|
|
385
|
+
storyBranch,
|
|
386
|
+
baseBranch,
|
|
387
|
+
prUrl,
|
|
388
|
+
prNumber,
|
|
389
|
+
autoMergeEnabled,
|
|
390
|
+
autoMergeReason,
|
|
391
|
+
worktreeReaped,
|
|
392
|
+
leaseReleased,
|
|
393
|
+
waitedForMerge: true,
|
|
394
|
+
merged: true,
|
|
395
|
+
});
|
|
396
|
+
Logger.info(
|
|
397
|
+
`\n--- STORY CLOSE RESULT ---\n${JSON.stringify(result, null, 2)}\n--- END RESULT ---\n`,
|
|
398
|
+
);
|
|
399
|
+
progress(
|
|
400
|
+
'DONE',
|
|
401
|
+
`✅ Standalone Story #${options.storyId}: PR merged → ${prUrl}`,
|
|
402
|
+
);
|
|
403
|
+
return { success: true, result };
|
|
404
|
+
}
|
|
405
|
+
|
|
344
406
|
const result = closeResult({
|
|
345
407
|
storyId: options.storyId,
|
|
346
408
|
storyBranch,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* spec-section-validator.js —
|
|
2
|
+
* spec-section-validator.js — Tech Spec post-authoring section gate.
|
|
3
3
|
*
|
|
4
4
|
* `/plan` Phase 7 authors a Tech Spec from documentation and the Epic body.
|
|
5
5
|
* Phase 8.3 (Holistic Consolidation) then reconciles the draft ticket array
|
|
@@ -12,19 +12,29 @@
|
|
|
12
12
|
*
|
|
13
13
|
* `validateSpecSections` parses a Tech Spec body for the required
|
|
14
14
|
* `## Delivery Slicing` heading (accepting the casing/wording variants
|
|
15
|
-
* below) and returns a deterministic `{ ok, missing[] }` envelope.
|
|
16
|
-
* caller (`epic-plan-spec-validate.js`) maps a non-empty `missing[]` to a
|
|
17
|
-
* non-zero exit so Phase 8 decomposition cannot proceed against an
|
|
18
|
-
* un-anchored spec.
|
|
15
|
+
* below) and returns a deterministic `{ ok, missing[] }` envelope.
|
|
19
16
|
*
|
|
20
|
-
*
|
|
17
|
+
* Story #4403 (Finding 3): this gate used to be run by a standalone
|
|
18
|
+
* `epic-plan-spec-validate.js` CLI as a separate Phase 7.5 workflow step —
|
|
19
|
+
* but by the time the documented ordering ran it, the Phase 7 persist path's
|
|
20
|
+
* `cleanupPhaseTempFiles` had already deleted the temp `techspec.md` file the
|
|
21
|
+
* gate read, so the "blocking gate" could never actually block. The CLI is
|
|
22
|
+
* retired; `runSpecPhase` (`phases/run-spec-phase.js`) now calls
|
|
23
|
+
* `validateSpecSections` directly against the in-memory authored content as
|
|
24
|
+
* part of its input validation, before any GitHub mutation. This is the
|
|
25
|
+
* Phase 8-side counterpart to the Phase 6 Epic Clarity Gate
|
|
21
26
|
* ({@link ../epic-plan-clarity.js#scoreEpicBody}) — same detect-then-prompt
|
|
22
|
-
* pattern, one phase later, but a hard gate (
|
|
27
|
+
* pattern, one phase later, but a hard gate (fail closed) rather than an
|
|
23
28
|
* advisory rubric.
|
|
24
29
|
*
|
|
25
|
-
*
|
|
30
|
+
* `validateSpecSections` and `formatMissingSectionMessage` are pure, I/O-free
|
|
31
|
+
* helpers. `validateSpecFile` is a thin disk-reading convenience for callers
|
|
32
|
+
* (tests, ad-hoc tooling) that hold a path rather than in-memory content.
|
|
26
33
|
*/
|
|
27
34
|
|
|
35
|
+
import { readFile } from 'node:fs/promises';
|
|
36
|
+
import { DELIVERY_SLICING_RE } from '../epic-body-sections.js';
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
39
|
* The single required Tech Spec section, with the heading variants the
|
|
30
40
|
* Architect persona may emit. Matched case-insensitively against any
|
|
@@ -39,7 +49,7 @@ const REQUIRED_SECTIONS = Object.freeze([
|
|
|
39
49
|
{
|
|
40
50
|
name: 'Delivery Slicing',
|
|
41
51
|
/** Level-2 heading, allowing the `Delivery ` qualifier to be optional. */
|
|
42
|
-
re:
|
|
52
|
+
re: DELIVERY_SLICING_RE,
|
|
43
53
|
},
|
|
44
54
|
]);
|
|
45
55
|
|
|
@@ -78,3 +88,44 @@ export function validateSpecSections({ body } = {}) {
|
|
|
78
88
|
|
|
79
89
|
return { ok: missing.length === 0, missing, present };
|
|
80
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate an authored Tech Spec file for the required post-authoring
|
|
94
|
+
* sections. Thin wrapper around `validateSpecSections` that owns the file
|
|
95
|
+
* read so callers holding a path (rather than in-memory content) do not each
|
|
96
|
+
* re-implement the read-then-validate sequence.
|
|
97
|
+
*
|
|
98
|
+
* @param {{ techspecPath: string }} args
|
|
99
|
+
* @returns {Promise<{ ok: boolean, missing: string[], present: string[] }>}
|
|
100
|
+
*/
|
|
101
|
+
export async function validateSpecFile({ techspecPath }) {
|
|
102
|
+
const body = await readFile(techspecPath, 'utf8');
|
|
103
|
+
return validateSpecSections({ body });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the operator-facing failure message for a missing-section result.
|
|
108
|
+
* Names each missing section and tells the operator whether to re-author the
|
|
109
|
+
* spec or add the section by hand before continuing.
|
|
110
|
+
*
|
|
111
|
+
* @param {{ techspecPath: string, missing: string[] }} args
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
export function formatMissingSectionMessage({ techspecPath, missing }) {
|
|
115
|
+
const list = missing.map((name) => `## ${name}`).join(', ');
|
|
116
|
+
return [
|
|
117
|
+
`[spec-section-validator] Tech Spec is missing required section(s): ${list}`,
|
|
118
|
+
` Spec source: ${techspecPath}`,
|
|
119
|
+
'',
|
|
120
|
+
` Phase 8 (decomposition) reconciles the draft ticket array against the`,
|
|
121
|
+
` Tech Spec's "## Delivery Slicing" section — without it, the Phase 8.3`,
|
|
122
|
+
` consolidation pass has no capability-boundary anchor and groups by`,
|
|
123
|
+
` technical shape instead.`,
|
|
124
|
+
'',
|
|
125
|
+
' To continue, do ONE of the following and re-run the Phase 7 persist step:',
|
|
126
|
+
` 1. Re-author the Tech Spec (re-run the Phase 7 spec-author step) so it`,
|
|
127
|
+
` emits a "## Delivery Slicing" section, OR`,
|
|
128
|
+
` 2. Add a "## Delivery Slicing" section to the Tech Spec by hand,`,
|
|
129
|
+
` describing the capability boundaries the work should be sliced along.`,
|
|
130
|
+
].join('\n');
|
|
131
|
+
}
|
|
@@ -376,7 +376,7 @@ function filterToStoryDiff({ miRows, crapRows, storyDiffPaths }) {
|
|
|
376
376
|
|
|
377
377
|
/**
|
|
378
378
|
* Check whether a `baseline-refresh-regression` signal tagged with the
|
|
379
|
-
* runner's `
|
|
379
|
+
* runner's `emitter.tool === 'auto-refresh-runner'` already exists in the
|
|
380
380
|
* Story's signals stream. Backs the AC3 idempotent-re-run contract.
|
|
381
381
|
*/
|
|
382
382
|
async function priorRefusalSignalExists({
|
|
@@ -391,7 +391,7 @@ async function priorRefusalSignalExists({
|
|
|
391
391
|
typeof record === 'object' &&
|
|
392
392
|
record.kind === 'friction' &&
|
|
393
393
|
record.category === FRICTION_CATEGORY &&
|
|
394
|
-
record?.
|
|
394
|
+
record?.emitter?.tool === RUNNER_SOURCE_TOOL
|
|
395
395
|
) {
|
|
396
396
|
found = true;
|
|
397
397
|
}
|
|
@@ -413,12 +413,14 @@ function buildRefusalSignal({
|
|
|
413
413
|
}) {
|
|
414
414
|
return {
|
|
415
415
|
kind: 'friction',
|
|
416
|
-
|
|
416
|
+
ts: new Date().toISOString(),
|
|
417
417
|
epicId,
|
|
418
418
|
storyId,
|
|
419
419
|
category: FRICTION_CATEGORY,
|
|
420
|
-
|
|
421
|
-
details:
|
|
420
|
+
emitter: { tool: RUNNER_SOURCE_TOOL },
|
|
421
|
+
details: {
|
|
422
|
+
message: `Auto-refresh refused: ${refusalReasons.length} row(s) breach configured caps (miDropCap=${caps.miDropCap}, crapJumpCap=${caps.crapJumpCap}).`,
|
|
423
|
+
},
|
|
422
424
|
refusalReasons,
|
|
423
425
|
miOverCap,
|
|
424
426
|
crapOverCap,
|
|
@@ -55,6 +55,10 @@ import {
|
|
|
55
55
|
buildMergeMessageWithCap,
|
|
56
56
|
loadHeaderMaxLength,
|
|
57
57
|
} from './merge-subject.js';
|
|
58
|
+
import {
|
|
59
|
+
assertNoForeignEpicLock as defaultAssertNoForeignEpicLock,
|
|
60
|
+
assertSharedCheckoutAvailable as defaultAssertSharedCheckoutAvailable,
|
|
61
|
+
} from './shared-checkout-guard.js';
|
|
58
62
|
|
|
59
63
|
/**
|
|
60
64
|
* Render the lock-file path for a given main-repo `cwd` + `epicId`. Pure;
|
|
@@ -424,7 +428,7 @@ export async function runFinalizeMerge({
|
|
|
424
428
|
storyBranch,
|
|
425
429
|
storyTitle,
|
|
426
430
|
storyId,
|
|
427
|
-
epicId
|
|
431
|
+
epicId,
|
|
428
432
|
cwd,
|
|
429
433
|
config,
|
|
430
434
|
bus = null,
|
|
@@ -432,6 +436,7 @@ export async function runFinalizeMerge({
|
|
|
432
436
|
logger = DefaultLogger,
|
|
433
437
|
gitSync = defaultGitSync,
|
|
434
438
|
gitSpawn = defaultGitSpawn,
|
|
439
|
+
assertSharedCheckoutAvailable = defaultAssertSharedCheckoutAvailable,
|
|
435
440
|
}) {
|
|
436
441
|
rebaseStoryOnEpic({
|
|
437
442
|
config,
|
|
@@ -443,6 +448,15 @@ export async function runFinalizeMerge({
|
|
|
443
448
|
gitSpawn,
|
|
444
449
|
});
|
|
445
450
|
|
|
451
|
+
// Story #4460 — cross-epic shared-checkout guard. Runs AFTER the
|
|
452
|
+
// per-Epic merge lock is already held (acquired around the whole close
|
|
453
|
+
// flow in story-close.js) so it composes with, rather than replaces,
|
|
454
|
+
// that same-epic serialization. Fails fast with an actionable
|
|
455
|
+
// diagnostic instead of letting a raw `git checkout` error surface
|
|
456
|
+
// when another epic's merge phase (or unrelated dirt) holds the
|
|
457
|
+
// shared checkout.
|
|
458
|
+
assertSharedCheckoutAvailable({ cwd, epicId, gitSpawn });
|
|
459
|
+
|
|
446
460
|
log('GIT', `Checking out ${epicBranch}...`);
|
|
447
461
|
gitSync(cwd, 'checkout', epicBranch);
|
|
448
462
|
gitSpawn(cwd, 'pull', '--rebase', 'origin', epicBranch);
|
|
@@ -582,13 +596,21 @@ export async function runResumeMerge({
|
|
|
582
596
|
storyBranch,
|
|
583
597
|
storyTitle,
|
|
584
598
|
storyId,
|
|
585
|
-
epicId
|
|
599
|
+
epicId,
|
|
586
600
|
config,
|
|
587
601
|
bus = null,
|
|
588
602
|
logger = DefaultLogger,
|
|
589
603
|
log = () => {},
|
|
590
604
|
gitSpawn = defaultGitSpawn,
|
|
605
|
+
assertNoForeignEpicLockFn = defaultAssertNoForeignEpicLock,
|
|
591
606
|
}) {
|
|
607
|
+
// Story #4460 follow-up: the resume path re-enters the shared checkout
|
|
608
|
+
// just like the finalize path, so another epic's live merge phase is
|
|
609
|
+
// the same hazard here. Only the foreign-lock half of the guard runs —
|
|
610
|
+
// a resume's own partial merge legitimately leaves the tree dirty, so
|
|
611
|
+
// the dirty-tree probe would false-positive against our own state.
|
|
612
|
+
assertNoForeignEpicLockFn({ cwd, epicId });
|
|
613
|
+
|
|
592
614
|
const resumeMergeMessage = await buildMergeMessage(storyTitle, storyId, {
|
|
593
615
|
cwd,
|
|
594
616
|
logger,
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Sits between the close-validation gate chain and the merge into
|
|
6
6
|
* `epic/<id>` inside `runStoryCloseLocked` (locked-pipeline.js). The
|
|
7
7
|
* configured ReviewProvider runs against the
|
|
8
|
-
* `epic/<id>`…`story-<id>` diff. The
|
|
9
|
-
* is posted to the Story issue (default
|
|
10
|
-
* inside `runCodeReview`). Outcomes:
|
|
8
|
+
* `epic/<id>`…`story-<id>` diff. The unified `verification-results`
|
|
9
|
+
* structured comment is posted to the Story issue (default
|
|
10
|
+
* `commentTargetId === ticketId` inside `runCodeReview`). Outcomes:
|
|
11
11
|
*
|
|
12
12
|
* - clean / non-critical findings → `{ blocked: null }`; the pipeline
|
|
13
13
|
* proceeds to merge.
|
|
@@ -32,10 +32,141 @@
|
|
|
32
32
|
* invocation pattern (Story #3653).
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import {
|
|
36
|
+
runAuditSuite,
|
|
37
|
+
selectLocalLenses,
|
|
38
|
+
} from '../../../audit-suite/index.js';
|
|
39
|
+
import { gitSpawn } from '../../../git-utils.js';
|
|
35
40
|
import { Logger } from '../../../Logger.js';
|
|
36
41
|
import { runCodeReview } from '../../code-review.js';
|
|
37
42
|
import { emitBlockedCloseResult } from '../merge-runner.js';
|
|
38
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The review depth the Story-scope local-lens pass runs at. Shift-left
|
|
46
|
+
* (Epic #4405): local concerns are cheap to decide on a single Story's diff, so
|
|
47
|
+
* the maker-blind Story-scope review runs its matched local lenses at `light`
|
|
48
|
+
* depth here rather than paying a deeper pass at Epic close. Fixed for this
|
|
49
|
+
* tier — it is not risk-scaled like the code-review pillar depth.
|
|
50
|
+
*/
|
|
51
|
+
export const STORY_SCOPE_LENS_DEPTH = 'light';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enumerate the files changed in the `baseRef...headRef` diff via
|
|
55
|
+
* `git diff --name-only`. Best-effort: returns `[]` when the diff cannot be
|
|
56
|
+
* enumerated (git failure, missing ref) and never throws, mirroring the
|
|
57
|
+
* advisory posture of the surrounding review phase. Synchronous `gitSpawn`
|
|
58
|
+
* (returns `{ status, stdout }`) is the same seam `code-review.js#countChangedFiles`
|
|
59
|
+
* uses.
|
|
60
|
+
*
|
|
61
|
+
* @param {{ baseRef: string, headRef: string, gitSpawnFn?: typeof gitSpawn }} args
|
|
62
|
+
* @returns {string[]} Changed file paths, or `[]` on any failure.
|
|
63
|
+
*/
|
|
64
|
+
export function enumerateChangedFiles({
|
|
65
|
+
baseRef,
|
|
66
|
+
headRef,
|
|
67
|
+
gitSpawnFn = gitSpawn,
|
|
68
|
+
}) {
|
|
69
|
+
try {
|
|
70
|
+
const result = gitSpawnFn(
|
|
71
|
+
process.cwd(),
|
|
72
|
+
'diff',
|
|
73
|
+
'--name-only',
|
|
74
|
+
`${baseRef}...${headRef}`,
|
|
75
|
+
);
|
|
76
|
+
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return result.stdout
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((f) => f.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run the Story-scope local-lens pass: select the LOCAL-tier lenses whose
|
|
90
|
+
* `filePatterns` match the actual Story diff (`baseRef...headRef`) and
|
|
91
|
+
* materialize their lens-prompt bodies at `light` depth. This is the
|
|
92
|
+
* shift-left tier from Epic #4405 — it runs **inside** the story-close
|
|
93
|
+
* subprocess spine (called only from {@link runStoryReviewCore}), never in the
|
|
94
|
+
* delivering child's (maker's) context, so a maker never grades its own work.
|
|
95
|
+
*
|
|
96
|
+
* A diff that matches no local lens adds **no** lens work: the roster is empty
|
|
97
|
+
* and `runAuditSuite` is never invoked. Best-effort and total — a git or
|
|
98
|
+
* materialization failure degrades to `{ skipped: true, lenses: [] }` and is
|
|
99
|
+
* logged via `progress`, matching the advisory posture the review phase already
|
|
100
|
+
* takes for provider/transport failures.
|
|
101
|
+
*
|
|
102
|
+
* @param {{
|
|
103
|
+
* baseRef: string,
|
|
104
|
+
* headRef: string,
|
|
105
|
+
* progress: (tag: string, msg: string) => void,
|
|
106
|
+
* progressTag?: string,
|
|
107
|
+
* gitSpawnFn?: typeof gitSpawn,
|
|
108
|
+
* selectLocalLensesFn?: typeof selectLocalLenses,
|
|
109
|
+
* runAuditSuiteFn?: typeof runAuditSuite,
|
|
110
|
+
* }} args
|
|
111
|
+
* @returns {Promise<{
|
|
112
|
+
* depth: 'light',
|
|
113
|
+
* lenses: string[],
|
|
114
|
+
* skipped: boolean,
|
|
115
|
+
* materialized: object|null,
|
|
116
|
+
* }>}
|
|
117
|
+
*/
|
|
118
|
+
export async function runLocalLensReview({
|
|
119
|
+
baseRef,
|
|
120
|
+
headRef,
|
|
121
|
+
progress,
|
|
122
|
+
progressTag = 'CODE-REVIEW',
|
|
123
|
+
gitSpawnFn = gitSpawn,
|
|
124
|
+
selectLocalLensesFn = selectLocalLenses,
|
|
125
|
+
runAuditSuiteFn = runAuditSuite,
|
|
126
|
+
}) {
|
|
127
|
+
const empty = {
|
|
128
|
+
depth: STORY_SCOPE_LENS_DEPTH,
|
|
129
|
+
lenses: [],
|
|
130
|
+
skipped: true,
|
|
131
|
+
materialized: null,
|
|
132
|
+
};
|
|
133
|
+
let lenses;
|
|
134
|
+
try {
|
|
135
|
+
const changedFiles = enumerateChangedFiles({
|
|
136
|
+
baseRef,
|
|
137
|
+
headRef,
|
|
138
|
+
gitSpawnFn,
|
|
139
|
+
});
|
|
140
|
+
lenses = selectLocalLensesFn({ changedFiles });
|
|
141
|
+
if (lenses.length === 0) {
|
|
142
|
+
progress(
|
|
143
|
+
progressTag,
|
|
144
|
+
'No local lens matched the Story diff — skipping the lens pass.',
|
|
145
|
+
);
|
|
146
|
+
return empty;
|
|
147
|
+
}
|
|
148
|
+
const materialized = await runAuditSuiteFn({ auditWorkflows: lenses });
|
|
149
|
+
progress(
|
|
150
|
+
progressTag,
|
|
151
|
+
`Ran ${lenses.length} local lens(es) at ${STORY_SCOPE_LENS_DEPTH} depth: ${lenses.join(', ')}.`,
|
|
152
|
+
);
|
|
153
|
+
return {
|
|
154
|
+
depth: STORY_SCOPE_LENS_DEPTH,
|
|
155
|
+
lenses,
|
|
156
|
+
skipped: false,
|
|
157
|
+
materialized,
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// The lens pass is advisory: a git or materialization failure must not
|
|
161
|
+
// fail the close. Log and degrade to a skipped envelope.
|
|
162
|
+
progress(
|
|
163
|
+
progressTag,
|
|
164
|
+
`⚠️ local lens pass failed (continuing without it): ${err?.message ?? err}`,
|
|
165
|
+
);
|
|
166
|
+
return empty;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
39
170
|
/**
|
|
40
171
|
* Collect the extra fields for the code-review-critical blocked envelope.
|
|
41
172
|
* Pure; used by `runStoryCodeReview` to populate the `extra` argument of
|
|
@@ -92,8 +223,13 @@ function buildCodeReviewBlockedExtra({ storyId, reviewResult }) {
|
|
|
92
223
|
* progressTag?: string,
|
|
93
224
|
* planningRisk?: { overallLevel?: ('low'|'medium'|'high'), axes?: Array<{ axis?: string, level?: string }> }|null,
|
|
94
225
|
* runCodeReviewFn?: typeof runCodeReview,
|
|
226
|
+
* runLocalLensReviewFn?: typeof runLocalLensReview,
|
|
95
227
|
* }} args
|
|
96
|
-
* @returns {Promise<object>} Raw result envelope from `runCodeReview
|
|
228
|
+
* @returns {Promise<object>} Raw result envelope from `runCodeReview`, augmented
|
|
229
|
+
* with a `localLensReview` field carrying the Story-scope local-lens pass
|
|
230
|
+
* outcome (Epic #4405, Story #4409). Both close entry points reach the lens
|
|
231
|
+
* pass through this single spine, so it runs on the Epic-attached and
|
|
232
|
+
* standalone paths alike and always inside the close subprocess.
|
|
97
233
|
*/
|
|
98
234
|
export async function runStoryReviewCore({
|
|
99
235
|
storyId,
|
|
@@ -105,6 +241,7 @@ export async function runStoryReviewCore({
|
|
|
105
241
|
progressTag = 'CODE-REVIEW',
|
|
106
242
|
planningRisk = null,
|
|
107
243
|
runCodeReviewFn = runCodeReview,
|
|
244
|
+
runLocalLensReviewFn = runLocalLensReview,
|
|
108
245
|
}) {
|
|
109
246
|
const storyIdNum = Number(storyId);
|
|
110
247
|
const opts = {
|
|
@@ -127,7 +264,21 @@ export async function runStoryReviewCore({
|
|
|
127
264
|
if (planningRisk != null) {
|
|
128
265
|
opts.planningRisk = planningRisk;
|
|
129
266
|
}
|
|
130
|
-
|
|
267
|
+
|
|
268
|
+
// Shift-left local-lens pass (Epic #4405). Runs matched local lenses at
|
|
269
|
+
// `light` depth against the actual Story diff, inside this close-subprocess
|
|
270
|
+
// spine so the maker never grades its own work. Advisory — it never blocks
|
|
271
|
+
// the close and its outcome rides on the returned envelope for downstream
|
|
272
|
+
// consumers.
|
|
273
|
+
const localLensReview = await runLocalLensReviewFn({
|
|
274
|
+
baseRef,
|
|
275
|
+
headRef,
|
|
276
|
+
progress,
|
|
277
|
+
progressTag,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const result = await runCodeReviewFn(opts);
|
|
281
|
+
return { ...result, localLensReview };
|
|
131
282
|
}
|
|
132
283
|
|
|
133
284
|
/**
|
|
@@ -155,8 +306,12 @@ export async function runStoryReviewCore({
|
|
|
155
306
|
* progress: (tag: string, msg: string) => void,
|
|
156
307
|
* planningRisk?: { overallLevel?: ('low'|'medium'|'high'), axes?: Array<{ axis?: string, level?: string }> }|null,
|
|
157
308
|
* runCodeReviewFn?: typeof runCodeReview,
|
|
309
|
+
* runLocalLensReviewFn?: typeof runLocalLensReview,
|
|
158
310
|
* }} args
|
|
159
|
-
* @returns {Promise<{ blocked: object|null }>}
|
|
311
|
+
* @returns {Promise<{ blocked: object|null, localLensReview?: object }>}
|
|
312
|
+
* `localLensReview` carries the Story-scope local-lens pass outcome
|
|
313
|
+
* (Epic #4405, Story #4409) when the review completed; it is absent only when
|
|
314
|
+
* the whole review phase threw (advisory failure).
|
|
160
315
|
*/
|
|
161
316
|
export async function runStoryCodeReview(args) {
|
|
162
317
|
const {
|
|
@@ -168,6 +323,7 @@ export async function runStoryCodeReview(args) {
|
|
|
168
323
|
progress,
|
|
169
324
|
planningRisk = null,
|
|
170
325
|
runCodeReviewFn = runCodeReview,
|
|
326
|
+
runLocalLensReviewFn = runLocalLensReview,
|
|
171
327
|
} = args;
|
|
172
328
|
|
|
173
329
|
const storyIdNum = Number(storyId);
|
|
@@ -186,6 +342,7 @@ export async function runStoryCodeReview(args) {
|
|
|
186
342
|
progress,
|
|
187
343
|
planningRisk,
|
|
188
344
|
runCodeReviewFn,
|
|
345
|
+
runLocalLensReviewFn,
|
|
189
346
|
});
|
|
190
347
|
} catch (err) {
|
|
191
348
|
// Adapter / wiring failure — log and proceed. The review is advisory
|
|
@@ -197,6 +354,8 @@ export async function runStoryCodeReview(args) {
|
|
|
197
354
|
return { blocked: null };
|
|
198
355
|
}
|
|
199
356
|
|
|
357
|
+
const localLensReview = reviewResult?.localLensReview;
|
|
358
|
+
|
|
200
359
|
if (reviewResult?.halted) {
|
|
201
360
|
const blocked = await emitBlockedCloseResult({
|
|
202
361
|
storyId: storyIdNum,
|
|
@@ -208,7 +367,7 @@ export async function runStoryCodeReview(args) {
|
|
|
208
367
|
blockedMessage: `Story #${storyIdNum} blocked: code-review reported ${reviewResult.severity.critical} critical blocker(s).`,
|
|
209
368
|
logger: Logger,
|
|
210
369
|
});
|
|
211
|
-
return { blocked };
|
|
370
|
+
return { blocked, localLensReview };
|
|
212
371
|
}
|
|
213
372
|
|
|
214
373
|
const counts = reviewResult?.severity ?? {};
|
|
@@ -216,5 +375,5 @@ export async function runStoryCodeReview(args) {
|
|
|
216
375
|
'CODE-REVIEW',
|
|
217
376
|
`Review complete — high=${counts.high ?? 0} medium=${counts.medium ?? 0} suggestion=${counts.suggestion ?? 0} (posted=${reviewResult?.posted ?? false}).`,
|
|
218
377
|
);
|
|
219
|
-
return { blocked: null };
|
|
378
|
+
return { blocked: null, localLensReview };
|
|
220
379
|
}
|
|
@@ -133,7 +133,14 @@ export async function runPreMergeGates({
|
|
|
133
133
|
// `buildDefaultGates` reads the canonical resolved config directly:
|
|
134
134
|
// gate commands resolve from `project.commands` and the CRAP toggle
|
|
135
135
|
// from `delivery.quality.gates.crap.enabled`.
|
|
136
|
-
|
|
136
|
+
// Probe the coverage script from the gate execution directory (the Story
|
|
137
|
+
// worktree when present) so coverage-capture is only registered when the
|
|
138
|
+
// consumer ships `test:coverage` (#4473).
|
|
139
|
+
const gates = buildDefaultGates({
|
|
140
|
+
config,
|
|
141
|
+
epicBranch,
|
|
142
|
+
cwd: worktreePath || cwd,
|
|
143
|
+
});
|
|
137
144
|
const gateCount = Array.isArray(gates) ? gates.length : 0;
|
|
138
145
|
// Story #2250 — emit `close-validate.start` only when both an epicId
|
|
139
146
|
// and a storyId are present; the schema requires both, and unit
|