mandrel 2.59.0 → 2.60.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 +11 -9
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +6 -6
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +8 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +4 -5
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -25
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -15
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -28
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +65 -54
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +22 -21
- package/.agents/workflows/mandrel-update.md +36 -21
- package/docs/CHANGELOG.md +35 -0
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +2 -1
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* @module lib/orchestration/story-follow-ups
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { orchestrationLogDir, signalsFile } from '../config/temp-paths.js';
|
|
13
16
|
import { graduateRetroProposals } from '../feedback-loop/retro-proposals-graduator.js';
|
|
14
17
|
import {
|
|
15
18
|
DEFAULT_FRAMEWORK_REPO,
|
|
@@ -30,6 +33,76 @@ import { upsertStructuredComment } from './ticketing.js';
|
|
|
30
33
|
|
|
31
34
|
const FOLLOW_UPS_COMMENT_TYPE = 'follow-ups';
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Publish a rendered roll-up — as a `follow-ups` comment when the run actually
|
|
38
|
+
* filed something, and as a run artifact under the temp root when it did not.
|
|
39
|
+
*
|
|
40
|
+
* Story #5341 made the filing the condition. The comment had been
|
|
41
|
+
* unconditional, and the corpus it rendered is dominated by noise: #5324's
|
|
42
|
+
* roll-up carried 116 signals and filed nothing, and #4653 / #4833 / #4834 /
|
|
43
|
+
* #4836 record filings that were false or leaked from fixtures. A comment that
|
|
44
|
+
* says "nothing to do" on every Story trains its readers to skip the one that
|
|
45
|
+
* does not — while the roll-up itself is still written, so nothing is lost,
|
|
46
|
+
* only moved off the ticket.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} args
|
|
49
|
+
* @param {number} args.anchorId — the ticket the roll-up belongs to.
|
|
50
|
+
* @param {string} args.body — the rendered roll-up.
|
|
51
|
+
* @param {number} args.filedCount — issues this roll-up actually filed.
|
|
52
|
+
* @param {object} args.provider
|
|
53
|
+
* @param {object} [args.config]
|
|
54
|
+
* @returns {Promise<{ posted: boolean, artifactPath: string|null, summary: string }>}
|
|
55
|
+
*/
|
|
56
|
+
export async function publishFollowUpsRollup({
|
|
57
|
+
anchorId,
|
|
58
|
+
body,
|
|
59
|
+
filedCount,
|
|
60
|
+
provider,
|
|
61
|
+
config,
|
|
62
|
+
}) {
|
|
63
|
+
return filedCount > 0
|
|
64
|
+
? postFollowUpsComment({ anchorId, body, filedCount, provider })
|
|
65
|
+
: parkFollowUpsRollup({ anchorId, body, config });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Post the roll-up as the `follow-ups` structured comment.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} args
|
|
72
|
+
* @returns {Promise<{ posted: true, artifactPath: null, summary: string }>}
|
|
73
|
+
*/
|
|
74
|
+
async function postFollowUpsComment({ anchorId, body, filedCount, provider }) {
|
|
75
|
+
await upsertStructuredComment(
|
|
76
|
+
provider,
|
|
77
|
+
anchorId,
|
|
78
|
+
FOLLOW_UPS_COMMENT_TYPE,
|
|
79
|
+
body,
|
|
80
|
+
);
|
|
81
|
+
return {
|
|
82
|
+
posted: true,
|
|
83
|
+
artifactPath: null,
|
|
84
|
+
summary: `Captured follow-ups for #${anchorId} (filed=${filedCount}).`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Write the roll-up to the run artifacts under the temp root.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} args
|
|
92
|
+
* @returns {Promise<{ posted: false, artifactPath: string, summary: string }>}
|
|
93
|
+
*/
|
|
94
|
+
async function parkFollowUpsRollup({ anchorId, body, config }) {
|
|
95
|
+
const dir = orchestrationLogDir(config);
|
|
96
|
+
const artifactPath = path.join(dir, `follow-ups-rollup-${anchorId}.md`);
|
|
97
|
+
await mkdir(dir, { recursive: true });
|
|
98
|
+
await writeFile(artifactPath, `${body}\n`, 'utf8');
|
|
99
|
+
return {
|
|
100
|
+
posted: false,
|
|
101
|
+
artifactPath,
|
|
102
|
+
summary: `No follow-ups filed for #${anchorId} — roll-up kept at ${artifactPath}.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
33
106
|
/** Milliseconds in one day — the unit `frictionWindowDays` is expressed in. */
|
|
34
107
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
35
108
|
|
|
@@ -708,6 +781,52 @@ export function buildFollowUpsCommentBody({
|
|
|
708
781
|
return lines.join('\n');
|
|
709
782
|
}
|
|
710
783
|
|
|
784
|
+
/**
|
|
785
|
+
* Compose the routed proposals for one Story's friction corpus.
|
|
786
|
+
*
|
|
787
|
+
* @param {number} sid
|
|
788
|
+
* @param {Array<object>} signals
|
|
789
|
+
* @param {object} [config]
|
|
790
|
+
* @returns {object}
|
|
791
|
+
*/
|
|
792
|
+
function composeStoryProposals(sid, signals, config) {
|
|
793
|
+
const repos = resolveFollowUpRepos(config);
|
|
794
|
+
return composeRoutedProposals({
|
|
795
|
+
anchorId: sid,
|
|
796
|
+
anchorKind: 'story',
|
|
797
|
+
frameworkRepo: repos.frameworkRepo,
|
|
798
|
+
consumerRepo: repos.consumerRepo,
|
|
799
|
+
signals,
|
|
800
|
+
// Derived, not hardcoded `[]` (Story #4649). This is the escape hatch
|
|
801
|
+
// the retired story-scope threshold carve-out was standing in for: a
|
|
802
|
+
// Story still parked at `agent::blocked` files at a single occurrence,
|
|
803
|
+
// while one that blocked and self-resolved nets out entirely.
|
|
804
|
+
unresolvedBlockedEvents: deriveUnresolvedBlockedEvents(signals),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Hand one Story's routed proposals to the graduator.
|
|
810
|
+
*
|
|
811
|
+
* @param {object} args
|
|
812
|
+
* @returns {Promise<object>}
|
|
813
|
+
*/
|
|
814
|
+
function fileStoryProposals({ sid, proposals, provider, config, cwd }) {
|
|
815
|
+
const repos = resolveFollowUpRepos(config);
|
|
816
|
+
return graduateRetroProposals({
|
|
817
|
+
epicId: sid,
|
|
818
|
+
provider,
|
|
819
|
+
config,
|
|
820
|
+
currentRepo: repos.currentRepo,
|
|
821
|
+
// The resolved bucket object, not a re-split of the slug: routing is
|
|
822
|
+
// decided once in `github/framework-repo.js`.
|
|
823
|
+
frameworkRepo: repos.repos.framework,
|
|
824
|
+
platformRepo: repos.repos.platform,
|
|
825
|
+
routedProposals: proposals,
|
|
826
|
+
cwd,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
711
830
|
/**
|
|
712
831
|
* Capture and persist Story follow-ups. Never throws — the land must not
|
|
713
832
|
* fail because follow-up filing flaked.
|
|
@@ -743,29 +862,12 @@ export async function captureStoryFollowUps({
|
|
|
743
862
|
}
|
|
744
863
|
try {
|
|
745
864
|
const signals = await gatherStoryFrictionSignals(sid, config);
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
frameworkRepo: repos.frameworkRepo,
|
|
751
|
-
consumerRepo: repos.consumerRepo,
|
|
752
|
-
signals,
|
|
753
|
-
// Derived, not hardcoded `[]` (Story #4649). This is the escape hatch
|
|
754
|
-
// the retired story-scope threshold carve-out was standing in for: a
|
|
755
|
-
// Story still parked at `agent::blocked` files at a single occurrence,
|
|
756
|
-
// while one that blocked and self-resolved nets out entirely.
|
|
757
|
-
unresolvedBlockedEvents: deriveUnresolvedBlockedEvents(signals),
|
|
758
|
-
});
|
|
759
|
-
const graduated = await graduateRetroProposals({
|
|
760
|
-
epicId: sid,
|
|
865
|
+
const proposals = composeStoryProposals(sid, signals, config);
|
|
866
|
+
const graduated = await fileStoryProposals({
|
|
867
|
+
sid,
|
|
868
|
+
proposals,
|
|
761
869
|
provider,
|
|
762
870
|
config,
|
|
763
|
-
currentRepo: repos.currentRepo,
|
|
764
|
-
// The resolved bucket object, not a re-split of the slug: routing is
|
|
765
|
-
// decided once in `github/framework-repo.js`.
|
|
766
|
-
frameworkRepo: repos.repos.framework,
|
|
767
|
-
platformRepo: repos.repos.platform,
|
|
768
|
-
routedProposals: proposals,
|
|
769
871
|
cwd,
|
|
770
872
|
});
|
|
771
873
|
const body = buildFollowUpsCommentBody({
|
|
@@ -775,30 +877,45 @@ export async function captureStoryFollowUps({
|
|
|
775
877
|
signalCount: signals.length,
|
|
776
878
|
categories: summarizeSignalCategories(signals),
|
|
777
879
|
});
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
880
|
+
const filedCount = graduated.filed?.length ?? 0;
|
|
881
|
+
const published = await publishFollowUpsRollup({
|
|
882
|
+
anchorId: sid,
|
|
883
|
+
body,
|
|
884
|
+
filedCount,
|
|
885
|
+
provider,
|
|
886
|
+
config,
|
|
887
|
+
});
|
|
888
|
+
progress?.('FOLLOW-UPS', published.summary);
|
|
783
889
|
return {
|
|
784
890
|
ok: true,
|
|
785
891
|
storyId: sid,
|
|
786
892
|
proposals,
|
|
787
893
|
graduated,
|
|
788
894
|
signalCount: signals.length,
|
|
895
|
+
commentPosted: published.posted,
|
|
896
|
+
artifactPath: published.artifactPath,
|
|
789
897
|
};
|
|
790
898
|
} catch (err) {
|
|
791
|
-
|
|
792
|
-
`[story-follow-ups] capture failed for #${sid}: ${err?.message ?? err}`,
|
|
793
|
-
);
|
|
794
|
-
progress?.(
|
|
795
|
-
'FOLLOW-UPS',
|
|
796
|
-
`⚠️ Follow-up capture failed (close continues): ${err?.message ?? err}`,
|
|
797
|
-
);
|
|
798
|
-
return {
|
|
799
|
-
ok: false,
|
|
800
|
-
reason: 'capture-failed',
|
|
801
|
-
error: String(err?.message ?? err),
|
|
802
|
-
};
|
|
899
|
+
return reportCaptureFailure(sid, err, progress);
|
|
803
900
|
}
|
|
804
901
|
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Report a failed capture without failing the close around it — follow-up
|
|
905
|
+
* capture is a reporting step, so a provider or filesystem fault degrades the
|
|
906
|
+
* report and never the land.
|
|
907
|
+
*
|
|
908
|
+
* @param {number} sid
|
|
909
|
+
* @param {unknown} err
|
|
910
|
+
* @param {((tag: string, msg: string) => void)} [progress]
|
|
911
|
+
* @returns {{ ok: false, reason: string, error: string }}
|
|
912
|
+
*/
|
|
913
|
+
function reportCaptureFailure(sid, err, progress) {
|
|
914
|
+
const detail = String(err?.message ?? err);
|
|
915
|
+
Logger.warn(`[story-follow-ups] capture failed for #${sid}: ${detail}`);
|
|
916
|
+
progress?.(
|
|
917
|
+
'FOLLOW-UPS',
|
|
918
|
+
`⚠️ Follow-up capture failed (close continues): ${detail}`,
|
|
919
|
+
);
|
|
920
|
+
return { ok: false, reason: 'capture-failed', error: detail };
|
|
921
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-init-envelope.js — read back the envelope `single-story-init.js`
|
|
3
|
+
* writes for one Story.
|
|
4
|
+
*
|
|
5
|
+
* Init routes its full result object to
|
|
6
|
+
* `<tempRoot>/orchestration/story-init-result-<id>.log` through
|
|
7
|
+
* `emitTerseResult`, wrapped in the same `--- STORY INIT RESULT ---` markers
|
|
8
|
+
* the legacy inline dump used. Story #5343 retired the `story-init` ticket
|
|
9
|
+
* comment that duplicated it, which makes this file the durable record of
|
|
10
|
+
* what a run was seeded with — most load-bearingly the `runScopedConfig` pin
|
|
11
|
+
* `run-scoped-config.js` compares against at close.
|
|
12
|
+
*
|
|
13
|
+
* Total and non-throwing: a missing, unreadable or unparseable envelope
|
|
14
|
+
* resolves to `null`, and the caller reports the degrade. Reading it is
|
|
15
|
+
* always best-effort — the temp tree is reapable by design.
|
|
16
|
+
*
|
|
17
|
+
* @module lib/orchestration/story-init-envelope
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { orchestrationLogDir } from '../config/temp-paths.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read one Story's init envelope, or `null` when there is none to read.
|
|
27
|
+
*
|
|
28
|
+
* @param {{
|
|
29
|
+
* storyId: number|string,
|
|
30
|
+
* config?: object,
|
|
31
|
+
* readFileFn?: typeof readFileSync,
|
|
32
|
+
* }} args
|
|
33
|
+
* @returns {object|null} the parsed init result, or `null`.
|
|
34
|
+
*/
|
|
35
|
+
function readStoryInitEnvelope({ storyId, config, readFileFn = readFileSync }) {
|
|
36
|
+
const file = path.join(
|
|
37
|
+
orchestrationLogDir(config),
|
|
38
|
+
`story-init-result-${storyId}.log`,
|
|
39
|
+
);
|
|
40
|
+
try {
|
|
41
|
+
// The log brackets the pretty JSON with human markers, so take the
|
|
42
|
+
// outermost brace span rather than parsing the whole file.
|
|
43
|
+
const fence = /\{[\s\S]*\}/.exec(readFileFn(file, 'utf8'));
|
|
44
|
+
const payload = fence ? JSON.parse(fence[0]) : null;
|
|
45
|
+
return payload && typeof payload === 'object' ? payload : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `runScopedConfig` pin off one Story's init envelope, or `null` when the
|
|
53
|
+
* envelope is absent or carries none. The write half is
|
|
54
|
+
* `run-scoped-config.js#pinRunScopedConfig`, which `single-story-init.js`
|
|
55
|
+
* records on the envelope this reads back.
|
|
56
|
+
*
|
|
57
|
+
* @param {{
|
|
58
|
+
* storyId: number|string,
|
|
59
|
+
* config?: object,
|
|
60
|
+
* readEnvelopeFn?: typeof readStoryInitEnvelope,
|
|
61
|
+
* }} args
|
|
62
|
+
* @returns {Record<string, unknown>|null}
|
|
63
|
+
*/
|
|
64
|
+
export function readRunScopedPin({
|
|
65
|
+
storyId,
|
|
66
|
+
config,
|
|
67
|
+
readEnvelopeFn = readStoryInitEnvelope,
|
|
68
|
+
}) {
|
|
69
|
+
const pin = readEnvelopeFn({ storyId, config })?.runScopedConfig;
|
|
70
|
+
return pin && typeof pin === 'object' ? pin : null;
|
|
71
|
+
}
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
* the 2-tier hierarchy (Epic → Story).
|
|
20
20
|
*
|
|
21
21
|
* Required after parse/normalize: a non-empty `goal`, and non-empty
|
|
22
|
-
* `changes
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `changes` and `acceptance` arrays — and `changes` items must name at least
|
|
23
|
+
* one path-shaped token so vague verbs ("clean up", "refactor") can't slip
|
|
24
|
+
* through. Story #5342 dropped the non-empty `verify` requirement: an empty
|
|
25
|
+
* `verify[]` is a dry-run warning the ticket validator raises, not a
|
|
26
|
+
* refusal.
|
|
25
27
|
*
|
|
26
28
|
* `acceptance` / `verify` are the **top-level machine contract** (Story
|
|
27
29
|
* #4541). The decomposer prompt tells authors to write those lists once at
|
|
@@ -43,8 +45,8 @@
|
|
|
43
45
|
*
|
|
44
46
|
* `body.verify` entries are commands, nothing more: Story #5312 deleted the
|
|
45
47
|
* `(<tier>)` suffix, the `manual:<reason>` escape and the repair pass that
|
|
46
|
-
* appended the suffix for the author
|
|
47
|
-
*
|
|
48
|
+
* appended the suffix for the author, and Story #5342 deleted the last rule
|
|
49
|
+
* — the non-empty check. This validator no longer scores `verify` at all.
|
|
48
50
|
*
|
|
49
51
|
* The errors are batched and surfaced as a single thrown Error so the
|
|
50
52
|
* planner can see every offending slug in one pass instead of fixing one
|
|
@@ -69,7 +71,7 @@ import { FILE_ASSUMPTION_VALUES } from './file-assumption-enum.js';
|
|
|
69
71
|
* Story body to a markdown string, so a *string* body is NOT skipped here
|
|
70
72
|
* (Story #3906) — `validateTaskBodyShape` parses it back into structured
|
|
71
73
|
* form via `parseStoryBody` before applying the section rules. This is what
|
|
72
|
-
* makes the
|
|
74
|
+
* makes the vague-verb / non-empty-goal checks actually fire
|
|
73
75
|
* on real plans. Features (and everything else) use narrative string bodies
|
|
74
76
|
* and are skipped by the `type !== 'story'` guard.
|
|
75
77
|
*
|
|
@@ -185,7 +187,6 @@ export function validateTaskBodyShape(ticket) {
|
|
|
185
187
|
}
|
|
186
188
|
errors.push(...collectChangesErrors(prefix, body.changes));
|
|
187
189
|
errors.push(...collectAcceptanceErrors(prefix, body.acceptance));
|
|
188
|
-
errors.push(...collectVerifyErrors(prefix, body.verify));
|
|
189
190
|
errors.push(...collectReferencesErrors(prefix, body.references));
|
|
190
191
|
return errors;
|
|
191
192
|
}
|
|
@@ -306,16 +307,6 @@ function collectAcceptanceErrors(prefix, rawAcceptance) {
|
|
|
306
307
|
* @param {unknown} rawVerify
|
|
307
308
|
* @returns {string[]}
|
|
308
309
|
*/
|
|
309
|
-
function collectVerifyErrors(prefix, rawVerify) {
|
|
310
|
-
const verify = Array.isArray(rawVerify) ? rawVerify : [];
|
|
311
|
-
if (verify.length === 0) {
|
|
312
|
-
return [
|
|
313
|
-
`${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section.`,
|
|
314
|
-
];
|
|
315
|
-
}
|
|
316
|
-
return [];
|
|
317
|
-
}
|
|
318
|
-
|
|
319
310
|
/**
|
|
320
311
|
* Validate every 2-tier Story in `tickets` whose `body` is a structured
|
|
321
312
|
* object. Returns an array of error strings (one per offending slug); empty
|
|
@@ -3,7 +3,6 @@ import { normalizeOwnedProvenance } from '../findings/provenance-field.js';
|
|
|
3
3
|
import { detectCycle } from '../Graph.js';
|
|
4
4
|
import { gitSpawn } from '../git-utils.js';
|
|
5
5
|
|
|
6
|
-
import { Logger } from '../Logger.js';
|
|
7
6
|
import { validateStoryFileAssumptions } from './file-assumptions.js';
|
|
8
7
|
import { isExternalDependencyRef } from './plan-persist/external-deps.js';
|
|
9
8
|
import {
|
|
@@ -41,38 +40,6 @@ function collectPathsFromText(text, paths) {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
* Resolve every acceptance line a Story declares, across both authoring
|
|
46
|
-
* shapes (Story #4541).
|
|
47
|
-
*
|
|
48
|
-
* The canonical shape is a **serialized string body** with the criteria at
|
|
49
|
-
* the ticket's **top level** — the machine contract persist syncs into the
|
|
50
|
-
* body. `validateAcceptanceSubjectPrefix` used to read `body.acceptance` on
|
|
51
|
-
* an object body only, so on every real plan it scanned nothing and the gate
|
|
52
|
-
* silently passed. Union both sources (deduplicated) so the gate fires on
|
|
53
|
-
* whichever surface the author used.
|
|
54
|
-
*
|
|
55
|
-
* @param {object} story
|
|
56
|
-
* @returns {string[]}
|
|
57
|
-
*/
|
|
58
|
-
function resolveAcceptanceLines(story) {
|
|
59
|
-
const lines = new Set();
|
|
60
|
-
if (Array.isArray(story?.acceptance)) {
|
|
61
|
-
for (const item of story.acceptance) lines.add(String(item ?? ''));
|
|
62
|
-
}
|
|
63
|
-
const body = story?.body;
|
|
64
|
-
let bodyAcceptance = null;
|
|
65
|
-
if (typeof body === 'string' && body.trim().length > 0) {
|
|
66
|
-
bodyAcceptance = parseStoryBodyOrThrow(story).acceptance;
|
|
67
|
-
} else if (body !== null && typeof body === 'object') {
|
|
68
|
-
bodyAcceptance = body.acceptance;
|
|
69
|
-
}
|
|
70
|
-
if (Array.isArray(bodyAcceptance)) {
|
|
71
|
-
for (const item of bodyAcceptance) lines.add(String(item ?? ''));
|
|
72
|
-
}
|
|
73
|
-
return [...lines];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
43
|
function collectTaskPathReferences(task) {
|
|
77
44
|
const paths = new Set();
|
|
78
45
|
const body = task.body;
|
|
@@ -283,117 +250,6 @@ export function validateAcFreshness({
|
|
|
283
250
|
return misses.map((m) => renderMissLine(m, baseBranchRef));
|
|
284
251
|
}
|
|
285
252
|
|
|
286
|
-
/**
|
|
287
|
-
* Allowed leading Conventional-Commits types. Mirrors the `changelog-sections`
|
|
288
|
-
* keys in `release-please-config.json` and the `type-enum` list in
|
|
289
|
-
* `commitlint.config.js`. When a planner LLM prescribes a commit subject in a
|
|
290
|
-
* Task acceptance item via the "Commit subject begins with '<prefix>:'" form,
|
|
291
|
-
* the captured prefix must reduce to one of these types (optionally followed
|
|
292
|
-
* by a `(scope)` qualifier) — anything else fails commitlint locally and
|
|
293
|
-
* release-please's changelog parser on `main`, so the decompose is rejected
|
|
294
|
-
* before the Story branch is ever cut.
|
|
295
|
-
*
|
|
296
|
-
* Epic #2501 introduced this guard after the legacy `baseline-refresh`
|
|
297
|
-
* leading-token prescription created a wave of commit-msg hook failures
|
|
298
|
-
* across story-deliver sub-agents. See
|
|
299
|
-
* `.agents/skills/core/gates-and-baselines/SKILL.md` for the canonical refresh
|
|
300
|
-
* shape (Conventional-Commits subject + `baseline-refresh: true` body
|
|
301
|
-
* trailer).
|
|
302
|
-
*/
|
|
303
|
-
const ALLOWED_COMMIT_TYPES = new Set([
|
|
304
|
-
'feat',
|
|
305
|
-
'fix',
|
|
306
|
-
'chore',
|
|
307
|
-
'refactor',
|
|
308
|
-
'perf',
|
|
309
|
-
'docs',
|
|
310
|
-
'style',
|
|
311
|
-
'test',
|
|
312
|
-
'build',
|
|
313
|
-
'ci',
|
|
314
|
-
'revert',
|
|
315
|
-
]);
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Regex matching the canonical "Commit subject begins with '<prefix>:'"
|
|
319
|
-
* prescription shape the planner emits in `body.acceptance[]` entries.
|
|
320
|
-
* The leading quote is captured loosely (single, double, or backtick) so the
|
|
321
|
-
* three quoting styles the decomposer LLM has historically emitted all
|
|
322
|
-
* match. The captured group is the prefix token *without* the trailing
|
|
323
|
-
* colon — callers normalize by stripping an optional `(scope)` qualifier
|
|
324
|
-
* before comparing against the allowed-types set.
|
|
325
|
-
*/
|
|
326
|
-
const SUBJECT_PREFIX_RE = /Commit subject begins with ['"`]([^'"`]+):['"`]/g;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Scan every Story's `body.acceptance[]` for "Commit subject begins with
|
|
330
|
-
* '<prefix>:'" prescriptions and reject the decompose when any captured
|
|
331
|
-
* prefix is not a valid Conventional-Commits type.
|
|
332
|
-
*
|
|
333
|
-
* A captured prefix of the form `chore(baselines)` is accepted — the
|
|
334
|
-
* leading `chore` is in the allowed-types set, and the `(scope)` qualifier
|
|
335
|
-
* is the standard Conventional-Commits scope shape. A captured prefix of
|
|
336
|
-
* the form `baseline-refresh` is rejected because no Conventional-Commits
|
|
337
|
-
* type starts with that token.
|
|
338
|
-
*
|
|
339
|
-
* Only acceptance criteria are scanned; `body.goal` / `body.verify` /
|
|
340
|
-
* `body.changes` are not commit-subject prescriptions by convention and
|
|
341
|
-
* scanning them would surface false positives from prose that happens to
|
|
342
|
-
* quote a forbidden prefix while explaining why it's forbidden.
|
|
343
|
-
*
|
|
344
|
-
* Both authoring shapes are covered (Story #4541): the canonical top-level
|
|
345
|
-
* `acceptance[]` on a serialized string body, and the pre-serialize
|
|
346
|
-
* `body.acceptance[]` object shape. Scanning only the latter made the gate
|
|
347
|
-
* inert on every real plan.
|
|
348
|
-
*
|
|
349
|
-
* @param {object} opts
|
|
350
|
-
* @param {object[]} opts.tickets - Validated ticket hierarchy.
|
|
351
|
-
* @throws {ValidationError} when one or more Story acceptance items
|
|
352
|
-
* prescribe a forbidden subject prefix. The error carries
|
|
353
|
-
* `code: 'forbidden-subject-prefix'` and a `violations[]` payload
|
|
354
|
-
* listing each `{ slug, prefix, line }` so the decompose loop can
|
|
355
|
-
* surface the exact offending text to the operator.
|
|
356
|
-
*/
|
|
357
|
-
export function validateAcceptanceSubjectPrefix({ tickets }) {
|
|
358
|
-
const violations = [];
|
|
359
|
-
const stories = (tickets ?? []).filter((t) => t.type === 'story');
|
|
360
|
-
for (const story of stories) {
|
|
361
|
-
for (const line of resolveAcceptanceLines(story)) {
|
|
362
|
-
// Reset the global regex between iterations.
|
|
363
|
-
SUBJECT_PREFIX_RE.lastIndex = 0;
|
|
364
|
-
let match = SUBJECT_PREFIX_RE.exec(line);
|
|
365
|
-
while (match !== null) {
|
|
366
|
-
const rawPrefix = match[1];
|
|
367
|
-
// Strip an optional `(scope)` qualifier — `chore(baselines)` reduces
|
|
368
|
-
// to `chore` for the allowed-types check.
|
|
369
|
-
const type = rawPrefix.replace(/\(.*\)$/, '').trim();
|
|
370
|
-
if (!ALLOWED_COMMIT_TYPES.has(type)) {
|
|
371
|
-
violations.push({
|
|
372
|
-
slug: story.slug ?? '<unknown>',
|
|
373
|
-
prefix: rawPrefix,
|
|
374
|
-
line,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
match = SUBJECT_PREFIX_RE.exec(line);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (violations.length === 0) return;
|
|
382
|
-
const allowed = [...ALLOWED_COMMIT_TYPES].join('|');
|
|
383
|
-
const lines = violations
|
|
384
|
-
.map(
|
|
385
|
-
(v) =>
|
|
386
|
-
` - "${v.slug}" → forbidden subject prefix "${v.prefix}:" in acceptance item: ${v.line}`,
|
|
387
|
-
)
|
|
388
|
-
.join('\n');
|
|
389
|
-
const err = new ValidationError(
|
|
390
|
-
`Cross-Validation Failed: ${violations.length} Story acceptance item(s) prescribe a non-Conventional-Commits subject prefix:\n${lines}\n\nAllowed leading types: ${allowed}. Use a Conventional-Commits subject (e.g. "chore(baselines): refresh ...") and a body trailer (e.g. "baseline-refresh: true") for machine-readable markers. See Epic #2501.`,
|
|
391
|
-
{ violations },
|
|
392
|
-
);
|
|
393
|
-
err.code = 'forbidden-subject-prefix';
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
253
|
/**
|
|
398
254
|
* Render one missing-path warning with a remediation hint pointing at the
|
|
399
255
|
* Story's `changes[]`. For `tests/**` paths we suggest the explicit
|
|
@@ -482,44 +338,55 @@ function assertAllTicketsAreStories({ tickets, stories }) {
|
|
|
482
338
|
}
|
|
483
339
|
|
|
484
340
|
/**
|
|
485
|
-
* Return true when a Story
|
|
486
|
-
*
|
|
487
|
-
* implementation unit and
|
|
488
|
-
*
|
|
341
|
+
* Return true when a Story carries a non-empty top-level `acceptance[]` —
|
|
342
|
+
* the inline-contract shape (Epic #3078) where the Story is itself the
|
|
343
|
+
* implementation unit and its criteria live on the Story rather than in
|
|
344
|
+
* child Task tickets.
|
|
489
345
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
* inline-contract invariant every Story must satisfy.
|
|
346
|
+
* Story #5342 narrowed the invariant to `acceptance[]` alone. A Story with
|
|
347
|
+
* no observable criterion is genuinely unimplementable and nothing
|
|
348
|
+
* downstream can recover it; an empty `verify[]` only means the deliverer
|
|
349
|
+
* picks the commands, which the close gate chain runs regardless — so that
|
|
350
|
+
* half is a warning ({@link collectMissingVerifyWarnings}), not a refusal.
|
|
496
351
|
*/
|
|
497
|
-
function
|
|
352
|
+
function hasInlineAcceptance(story) {
|
|
498
353
|
if (story === null || typeof story !== 'object') return false;
|
|
499
|
-
const { acceptance
|
|
500
|
-
return (
|
|
501
|
-
Array.isArray(acceptance) &&
|
|
502
|
-
acceptance.length > 0 &&
|
|
503
|
-
Array.isArray(verify) &&
|
|
504
|
-
verify.length > 0
|
|
505
|
-
);
|
|
354
|
+
const { acceptance } = story;
|
|
355
|
+
return Array.isArray(acceptance) && acceptance.length > 0;
|
|
506
356
|
}
|
|
507
357
|
|
|
508
358
|
function assertEveryStoryHasInlineContract({ stories }) {
|
|
509
|
-
|
|
510
|
-
// unit and MUST carry a non-empty inline contract — top-level
|
|
511
|
-
// `acceptance[]` AND `verify[]`. A Story missing either is the legacy
|
|
512
|
-
// 4-tier shape that expected child Tasks; there is no Task tier any
|
|
513
|
-
// more, so such a Story is unimplementable and the decompose is
|
|
514
|
-
// rejected outright.
|
|
515
|
-
const missing = stories.filter((s) => !hasInlineAcceptanceAndVerify(s));
|
|
359
|
+
const missing = stories.filter((s) => !hasInlineAcceptance(s));
|
|
516
360
|
if (missing.length === 0) return;
|
|
517
361
|
const list = missing.map((s) => `"${s.title}" (${s.slug})`).join(', ');
|
|
518
362
|
throw new Error(
|
|
519
|
-
`Cross-Validation Failed: ${missing.length} Story/Stories lack an inline acceptance
|
|
363
|
+
`Cross-Validation Failed: ${missing.length} Story/Stories lack an inline acceptance contract: ${list}. Every Story must carry a non-empty top-level acceptance[] — the outcomes a PR reviewer confirms once it lands.`,
|
|
520
364
|
);
|
|
521
365
|
}
|
|
522
366
|
|
|
367
|
+
/**
|
|
368
|
+
* One warning per Story with no `verify[]` entry (Story #5342).
|
|
369
|
+
*
|
|
370
|
+
* Demoted from the hard refusal above: an absent verify list costs the
|
|
371
|
+
* acceptance critic its cheapest evidence, which is worth saying on the
|
|
372
|
+
* dry-run, but it never makes the Story unimplementable — the deliverer
|
|
373
|
+
* derives the commands and the close gate chain runs either way.
|
|
374
|
+
*
|
|
375
|
+
* @param {object[]} stories
|
|
376
|
+
* @returns {string[]}
|
|
377
|
+
*/
|
|
378
|
+
function collectMissingVerifyWarnings(stories) {
|
|
379
|
+
return (stories ?? [])
|
|
380
|
+
.filter((s) => !Array.isArray(s?.verify) || s.verify.length === 0)
|
|
381
|
+
.map(
|
|
382
|
+
(s) =>
|
|
383
|
+
`Story "${s.slug ?? s.title ?? '<unknown>'}" lists no verify[] entry — ` +
|
|
384
|
+
'the deliverer and the acceptance critic have no mechanical check to ' +
|
|
385
|
+
'read as evidence. Add the exact command or test path unless the ' +
|
|
386
|
+
'Story genuinely has none.',
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
523
390
|
/**
|
|
524
391
|
* Shape-check the optional per-Story `provenance` field (Story #5045).
|
|
525
392
|
*
|
|
@@ -619,19 +486,12 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
619
486
|
assertAcyclic(slugAdjacency);
|
|
620
487
|
|
|
621
488
|
// Story #4541 — refuse an unparseable Story body up front, with a named
|
|
622
|
-
// error pointing at the offending section + entry. Must precede
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// Story had legitimately declared.
|
|
489
|
+
// error pointing at the offending section + entry. Must precede the
|
|
490
|
+
// freshness gate: it parses the body, and its net-new whitelist comes from
|
|
491
|
+
// `body.changes`, so a malformed body used to surface as a stale-path miss
|
|
492
|
+
// naming the paths the Story had legitimately declared.
|
|
627
493
|
assertStoryBodiesParse({ tickets });
|
|
628
494
|
|
|
629
|
-
// Reject any Task acceptance item that prescribes a non-Conventional-Commits
|
|
630
|
-
// subject prefix (e.g. legacy "Commit subject begins with 'baseline-refresh:'"
|
|
631
|
-
// from pre-Epic-#2501 planner output). Runs before the freshness gate so
|
|
632
|
-
// the failure mode is reported up-front rather than after a git probe.
|
|
633
|
-
validateAcceptanceSubjectPrefix({ tickets });
|
|
634
|
-
|
|
635
495
|
// Hoist a single memoized (ref, path) → boolean probe shared across both
|
|
636
496
|
// git-probe gates below. Without this, `validateAcFreshness` and
|
|
637
497
|
// `validateStoryFileAssumptions` each maintain an independent cache, so a
|
|
@@ -643,7 +503,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
643
503
|
? makeMemoizedGitRunner(opts.gitRunner ?? defaultGitRunner)
|
|
644
504
|
: null;
|
|
645
505
|
|
|
646
|
-
const warnings = [];
|
|
506
|
+
const warnings = [...collectMissingVerifyWarnings(stories)];
|
|
647
507
|
// Story #5312: a goal / acceptance / verify path absent at base is a
|
|
648
508
|
// warning the dry-run lists, not a refusal. Skipped when the caller omits
|
|
649
509
|
// `baseBranchRef` so unit tests keep their semantics; production
|
|
@@ -702,5 +562,6 @@ export const _internal = {
|
|
|
702
562
|
assertNoUnknownDeps,
|
|
703
563
|
assertAcyclic,
|
|
704
564
|
attachFindingsAndErrors,
|
|
705
|
-
|
|
565
|
+
hasInlineAcceptance,
|
|
566
|
+
collectMissingVerifyWarnings,
|
|
706
567
|
};
|