mandrel 1.87.0 → 1.89.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 +63 -16
- package/.agents/docs/configuration.md +5 -3
- 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 +16 -4
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -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/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/config/explain.js +4 -0
- package/.agents/scripts/lib/config/runners.js +21 -2
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
- 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 +66 -20
- 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/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 +188 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
- 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 +218 -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/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/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 +193 -44
- package/.agents/workflows/helpers/deliver-epic.md +128 -39
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -283
- 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 -207
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -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
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shared-checkout-guard.js — cross-epic contention guard for the merge
|
|
3
|
+
* phase's `git checkout <epicBranch>` in the shared main checkout
|
|
4
|
+
* (Story #4460).
|
|
5
|
+
*
|
|
6
|
+
* `story-close.js`'s merge phase (`runFinalizeMerge` in `merge-runner.js`)
|
|
7
|
+
* runs `git checkout <epicBranch>` directly in the shared main repo
|
|
8
|
+
* checkout — `close-inputs.js` resolves `cwd` to `PROJECT_ROOT`, not an
|
|
9
|
+
* isolated worktree. The only exclusivity guard around that shared
|
|
10
|
+
* checkout is the per-Epic `epic-merge-lock.js` mutex, which only
|
|
11
|
+
* serializes concurrent runs for the SAME epic. Nothing stops a
|
|
12
|
+
* DIFFERENT epic's concurrently-running `story-close.js` from treating
|
|
13
|
+
* the same shared checkout as scratch space at the same time.
|
|
14
|
+
*
|
|
15
|
+
* This was observed live: while delivering Epic #4425 Stories #4427/#4428,
|
|
16
|
+
* the shared checkout repeatedly carried uncommitted stray changes
|
|
17
|
+
* belonging to a concurrently-running Epic #4405 delivery (parked on
|
|
18
|
+
* `epic/4405` with dirty edits), which blocked the `git checkout epic/4425`
|
|
19
|
+
* merge step with a raw `error: Your local changes ... would be
|
|
20
|
+
* overwritten by checkout`.
|
|
21
|
+
*
|
|
22
|
+
* `assertSharedCheckoutAvailable` runs immediately before that checkout
|
|
23
|
+
* and fails fast with an actionable, story-close-specific diagnostic
|
|
24
|
+
* instead of letting the raw git error surface. It COMPOSES with (does
|
|
25
|
+
* not replace) the per-Epic lock: by the time this guard runs, the
|
|
26
|
+
* caller's own epic lock is already held (acquired around the whole
|
|
27
|
+
* close flow in `story-close.js`), so this guard only inspects OTHER
|
|
28
|
+
* epics' lock files plus the tree's overall dirty state — it never
|
|
29
|
+
* contends with same-epic concurrent runs, which continue to serialize
|
|
30
|
+
* solely via `withEpicMergeLock` before this guard ever executes.
|
|
31
|
+
*
|
|
32
|
+
* Breadth trade-off (deliberate): the foreign-lock probe keys on the
|
|
33
|
+
* OTHER epic's per-epic merge lock, which that run holds for its WHOLE
|
|
34
|
+
* close flow — so any overlapping story-close of another epic trips this
|
|
35
|
+
* guard even when that run never actually touches the shared checkout
|
|
36
|
+
* during the overlap. The refusal is deterministic and loud (throws with
|
|
37
|
+
* a diagnostic naming the holder pid), never a deadlock (no waiting),
|
|
38
|
+
* and stale/dead-pid foreign locks are ignored via the pid-liveness
|
|
39
|
+
* probe. Narrowing the window (a dedicated checkout-phase lock) was
|
|
40
|
+
* considered and rejected: the coarse refusal is rare, cheap to retry,
|
|
41
|
+
* and far simpler than a second lock tier.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { findForeignActiveEpicLock as defaultFindForeignActiveEpicLock } from '../../epic-merge-lock.js';
|
|
45
|
+
import { gitSpawn as defaultGitSpawn } from '../../git-utils.js';
|
|
46
|
+
|
|
47
|
+
const MAX_LISTED_DIRTY_FILES = 20;
|
|
48
|
+
|
|
49
|
+
function describeForeignLock(foreign) {
|
|
50
|
+
const acquired = Number.isFinite(foreign.acquiredAt)
|
|
51
|
+
? new Date(foreign.acquiredAt).toISOString()
|
|
52
|
+
: 'an unknown time';
|
|
53
|
+
return (
|
|
54
|
+
`[story-close] shared-checkout guard: refusing to touch the shared main checkout — ` +
|
|
55
|
+
`it is currently held by epic #${foreign.epicId}'s story-close merge phase ` +
|
|
56
|
+
`(lock ${foreign.filePath}, pid ${foreign.pid}, acquired ${acquired}). ` +
|
|
57
|
+
`Wait for that epic's story-close run to finish before retrying this merge. ` +
|
|
58
|
+
`If you have independently confirmed that process is no longer running, remove ` +
|
|
59
|
+
`the lock file by hand — never force a checkout past a live foreign lock. ` +
|
|
60
|
+
`See .agents/rules/git-conventions.md § Shared-checkout contention (Story #4460).`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function listDirtyFiles(porcelainOutput) {
|
|
65
|
+
// `git status --porcelain` lines are `XY <path>` — a fixed 2-char status
|
|
66
|
+
// column, a space, then the path. Strip only a trailing `\r` (Windows)
|
|
67
|
+
// before slicing off that 3-char prefix; trimming the whole line first
|
|
68
|
+
// would shift the slice offset whenever the status column starts with a
|
|
69
|
+
// space (the common "unstaged modification" case), truncating the path.
|
|
70
|
+
const lines = porcelainOutput
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => line.replace(/\r$/, ''))
|
|
73
|
+
.filter((line) => line.length > 0);
|
|
74
|
+
const shown = lines
|
|
75
|
+
.slice(0, MAX_LISTED_DIRTY_FILES)
|
|
76
|
+
.map((line) => line.slice(3).trim() || line);
|
|
77
|
+
const overflow = lines.length - shown.length;
|
|
78
|
+
return overflow > 0
|
|
79
|
+
? `${shown.join(', ')}, … (+${overflow} more)`
|
|
80
|
+
: shown.join(', ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function describeDirtyCheckout({ cwd, epicId, currentBranch, dirtyFiles }) {
|
|
84
|
+
return (
|
|
85
|
+
`[story-close] shared-checkout guard: refusing to check out the epic branch for ` +
|
|
86
|
+
`epic #${epicId} — the shared main checkout at ${cwd} is dirty (currently on ` +
|
|
87
|
+
`\`${currentBranch}\`). Dirty files: ${dirtyFiles}. This usually means another ` +
|
|
88
|
+
`epic's story-close run left uncommitted work in the shared checkout, or a prior ` +
|
|
89
|
+
`run crashed mid-merge. Resolve manually (stash/commit/reset in ${cwd}) before ` +
|
|
90
|
+
`retrying. See .agents/rules/git-conventions.md § Shared-checkout contention ` +
|
|
91
|
+
`(Story #4460).`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fail fast when the shared main checkout is not safely available for this
|
|
97
|
+
* epic's merge-phase `git checkout <epicBranch>` — either because another
|
|
98
|
+
* epic's story-close merge phase currently holds it (a live foreign lock),
|
|
99
|
+
* or because it is simply dirty (regardless of whose branch is checked
|
|
100
|
+
* out). Silent no-op when the checkout is clean and uncontended.
|
|
101
|
+
*
|
|
102
|
+
* @param {{
|
|
103
|
+
* cwd: string,
|
|
104
|
+
* epicId: number|string,
|
|
105
|
+
* gitSpawn?: typeof defaultGitSpawn,
|
|
106
|
+
* findForeignActiveEpicLock?: typeof defaultFindForeignActiveEpicLock,
|
|
107
|
+
* }} opts
|
|
108
|
+
* @throws {Error} with an actionable, story-close-specific diagnostic.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Foreign-lock-only variant for the RESUME merge path (Story #4460
|
|
112
|
+
* follow-up): a resume legitimately re-enters a shared checkout that is
|
|
113
|
+
* dirty with THIS story's own in-progress merge, so the dirty-tree half
|
|
114
|
+
* of `assertSharedCheckoutAvailable` would false-positive there. The
|
|
115
|
+
* cross-epic hazard — another epic's live merge phase holding the
|
|
116
|
+
* checkout — still applies and is the only probe this variant runs.
|
|
117
|
+
*/
|
|
118
|
+
export function assertNoForeignEpicLock({
|
|
119
|
+
cwd,
|
|
120
|
+
epicId,
|
|
121
|
+
findForeignActiveEpicLock = defaultFindForeignActiveEpicLock,
|
|
122
|
+
}) {
|
|
123
|
+
const foreign = findForeignActiveEpicLock(epicId, { repoRoot: cwd });
|
|
124
|
+
if (foreign) {
|
|
125
|
+
throw new Error(describeForeignLock(foreign));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function assertSharedCheckoutAvailable({
|
|
130
|
+
cwd,
|
|
131
|
+
epicId,
|
|
132
|
+
gitSpawn = defaultGitSpawn,
|
|
133
|
+
findForeignActiveEpicLock = defaultFindForeignActiveEpicLock,
|
|
134
|
+
}) {
|
|
135
|
+
const foreign = findForeignActiveEpicLock(epicId, { repoRoot: cwd });
|
|
136
|
+
if (foreign) {
|
|
137
|
+
throw new Error(describeForeignLock(foreign));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const statusRes = gitSpawn(cwd, 'status', '--porcelain');
|
|
141
|
+
if (statusRes.status !== 0) {
|
|
142
|
+
// Can't determine dirtiness from here — let the downstream checkout
|
|
143
|
+
// surface whatever git itself reports rather than guessing.
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const porcelain = statusRes.stdout || '';
|
|
147
|
+
if (porcelain.trim().length === 0) return;
|
|
148
|
+
|
|
149
|
+
const branchRes = gitSpawn(cwd, 'rev-parse', '--abbrev-ref', 'HEAD');
|
|
150
|
+
const currentBranch =
|
|
151
|
+
branchRes.status === 0
|
|
152
|
+
? (branchRes.stdout || '').trim() || 'unknown'
|
|
153
|
+
: 'unknown';
|
|
154
|
+
|
|
155
|
+
throw new Error(
|
|
156
|
+
describeDirtyCheckout({
|
|
157
|
+
cwd,
|
|
158
|
+
epicId,
|
|
159
|
+
currentBranch,
|
|
160
|
+
dirtyFiles: listDirtyFiles(porcelain),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -59,7 +59,17 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
59
59
|
'friction',
|
|
60
60
|
'notification',
|
|
61
61
|
// Extended set (Story #449 — retro follow-ons)
|
|
62
|
-
|
|
62
|
+
// Story #4411 (Epic #4405) — the former `code-review` structured comment
|
|
63
|
+
// is unified with the former `audit-results` comment into the single
|
|
64
|
+
// `verification-results` findings contract. `runCodeReview` (the sole code
|
|
65
|
+
// producer) upserts `verification-results`; the feedback-loop graduators and
|
|
66
|
+
// the auto-merge integration gate read it. Both the `code-review` and (as of
|
|
67
|
+
// Story #4412's slim-Epic-close cutover) the `audit-results` markers are
|
|
68
|
+
// retired here — the Phase 4 standalone lens walk folded into the Phase 5
|
|
69
|
+
// code-review pass, whose single `verification-results` comment now carries
|
|
70
|
+
// the Epic-close lens findings. Hard cutover, no dual-shape reader per
|
|
71
|
+
// `git-conventions.md`.
|
|
72
|
+
'verification-results',
|
|
63
73
|
'retro',
|
|
64
74
|
'retro-partial',
|
|
65
75
|
'epic-run-state',
|
|
@@ -98,14 +108,6 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
98
108
|
// operator can correct drift before Phase 8 decomposes from a stale
|
|
99
109
|
// spec. Advisory: the run continues regardless of the report contents.
|
|
100
110
|
'spec-freshness',
|
|
101
|
-
// Story #2681 — `/deliver` Phase 4 epic-audit helper upserts an
|
|
102
|
-
// `audit-results` comment on the Epic listing the per-lens findings
|
|
103
|
-
// returned by the change-set audit pass. The marker was prescribed by
|
|
104
|
-
// `helpers/epic-audit.md` Step 4 long before it was added to this
|
|
105
|
-
// registry; without the entry the helper's `post-structured-comment.js`
|
|
106
|
-
// invocation always failed with "Invalid structured-comment type". One
|
|
107
|
-
// entry per Epic; re-runs replace prior content.
|
|
108
|
-
'audit-results',
|
|
109
111
|
// Story #2813 — the per-Task progress writer (since retired under
|
|
110
112
|
// #3157) upserted a `model-attribution` comment on a Task ticket at
|
|
111
113
|
// the moment it transitioned to `agent::executing`, recording which
|
|
@@ -160,6 +162,15 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
160
162
|
// refuses with the claim age. One entry per Epic; re-acquires upsert
|
|
161
163
|
// in place.
|
|
162
164
|
'plan-lease',
|
|
165
|
+
// Story #4415 (Epic #4406) — the feedback-loop graduators
|
|
166
|
+
// (`audit-results-graduator.js` / `retro-proposals-graduator.js`) upsert a
|
|
167
|
+
// `cross-repo-deferred` comment on the Epic listing findings that route
|
|
168
|
+
// to a different repository and were therefore not filed here. Replaces
|
|
169
|
+
// the prior log-line-only trace so the deferral survives the finalize
|
|
170
|
+
// run as a durable, operator-visible record. Discriminated by a
|
|
171
|
+
// `graduator="audit-results|code-review"` attr so the two graduators
|
|
172
|
+
// upsert independent comments; re-runs upsert in place.
|
|
173
|
+
'cross-repo-deferred',
|
|
163
174
|
]);
|
|
164
175
|
|
|
165
176
|
export const WAVE_TYPE_PATTERN = WAVE_MARKER_RE;
|