mandrel 1.81.0 → 1.83.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 +46 -5
- package/.agents/docs/SDLC.md +97 -82
- package/.agents/docs/agentrc-reference.json +10 -2
- package/.agents/docs/configuration.md +4 -1
- package/.agents/docs/execution-reference.md +52 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +85 -45
- package/.agents/personas/architect.md +8 -5
- package/.agents/personas/engineer-mobile.md +3 -2
- package/.agents/personas/engineer-web.md +3 -2
- package/.agents/personas/engineer.md +6 -5
- package/.agents/personas/product.md +19 -13
- package/.agents/personas/project-manager.md +9 -8
- package/.agents/personas/qa-engineer.md +10 -6
- package/.agents/personas/refactorer.md +3 -2
- package/.agents/personas/technical-writer.md +2 -1
- package/.agents/personas/ux-designer.md +2 -2
- package/.agents/schemas/agentrc.schema.json +41 -3
- package/.agents/schemas/qa-ledger.schema.json +2 -2
- package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
- package/.agents/scripts/epic-deliver-prepare.js +40 -31
- package/.agents/scripts/epic-plan-decompose.js +2 -5
- package/.agents/scripts/epic-plan-spec.js +16 -19
- package/.agents/scripts/hierarchy-gate.js +11 -11
- package/.agents/scripts/lib/ITicketingProvider.js +4 -3
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +1 -1
- package/.agents/scripts/lib/cli-args.js +1 -5
- package/.agents/scripts/lib/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/explain.js +4 -1
- package/.agents/scripts/lib/config/temp-paths.js +1 -4
- package/.agents/scripts/lib/config-settings-schema.js +30 -1
- package/.agents/scripts/lib/epic-body-sections.js +310 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +38 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +15 -3
- package/.agents/scripts/lib/findings/promote-finding.js +3 -3
- package/.agents/scripts/lib/findings/severity.js +5 -6
- package/.agents/scripts/lib/label-constants.js +7 -17
- package/.agents/scripts/lib/label-taxonomy.js +4 -21
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +65 -2
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +105 -9
- package/.agents/scripts/lib/orchestration/doc-reader.js +29 -0
- package/.agents/scripts/lib/orchestration/docs-digest.js +134 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +23 -22
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +7 -10
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +4 -38
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +8 -9
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +11 -5
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +26 -5
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +102 -304
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +32 -29
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +19 -20
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +6 -9
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +3 -4
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +20 -27
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +11 -5
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +22 -59
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +1 -1
- package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +3 -3
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +15 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +122 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -8
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +2 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -2
- package/.agents/scripts/lib/qa/console-allowlist.js +5 -4
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +6 -85
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +144 -8
- package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -8
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/lint-label-vocabulary.js +2 -3
- package/.agents/scripts/providers/github/mappers.js +0 -3
- package/.agents/scripts/providers/github/tickets.js +7 -18
- package/.agents/scripts/single-story-init.js +0 -1
- package/.agents/scripts/story-init.js +1 -29
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +43 -22
- package/.agents/skills/core/epic-plan-consolidate/examples.md +51 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +27 -40
- package/.agents/skills/core/epic-plan-decompose-author/examples.md +47 -0
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +15 -13
- package/.agents/skills/core/epic-plan-premortem/examples.md +53 -0
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +143 -151
- package/.agents/skills/core/epic-plan-spec-author/examples.md +91 -0
- package/.agents/skills/core/hydrate-context/SKILL.md +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +3 -2
- package/.agents/skills/core/scope-triage/SKILL.md +2 -1
- package/.agents/skills/skills.index.json +8 -8
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +52 -38
- package/.agents/templates/epic-from-idea.md +4 -0
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/code-review.md +11 -9
- package/.agents/workflows/helpers/deliver-epic-reference.md +514 -0
- package/.agents/workflows/helpers/deliver-epic.md +173 -490
- package/.agents/workflows/helpers/epic-audit.md +11 -8
- package/.agents/workflows/helpers/epic-deliver-story.md +45 -27
- package/.agents/workflows/helpers/epic-plan-decompose.md +17 -12
- package/.agents/workflows/helpers/epic-plan-spec.md +68 -68
- package/.agents/workflows/helpers/parallel-tooling.md +2 -1
- package/.agents/workflows/helpers/plan-epic-reference.md +136 -0
- package/.agents/workflows/helpers/plan-epic.md +141 -256
- package/.agents/workflows/helpers/plan-story.md +31 -61
- package/.agents/workflows/helpers/qa-run-scenario.md +194 -0
- package/.agents/workflows/helpers/scope-triage-gate.md +97 -0
- package/.agents/workflows/helpers/single-story-deliver-reference.md +423 -0
- package/.agents/workflows/helpers/single-story-deliver.md +129 -393
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -1
- package/.agents/workflows/plan.md +8 -8
- package/.agents/workflows/qa-assist.md +2 -1
- package/.agents/workflows/qa-explore.md +63 -32
- package/.agents/workflows/qa-run.md +293 -130
- package/docs/CHANGELOG.md +35 -0
- package/package.json +1 -1
- package/.agents/schemas/qa-finding.schema.json +0 -133
- package/.agents/scripts/lib/issue-link-parser.js +0 -74
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +0 -116
- package/.agents/scripts/lib/orchestration/planning-state-manager.js +0 -318
- package/.agents/scripts/lib/story-init/hierarchy-tracer.js +0 -57
|
@@ -82,15 +82,15 @@ async function setEpicLabel(provider, epicId, targetLabel) {
|
|
|
82
82
|
*
|
|
83
83
|
* @param {number} epicId
|
|
84
84
|
* @param {import('../../../ITicketingProvider.js').ITicketingProvider} provider
|
|
85
|
-
* @param {{
|
|
85
|
+
* @param {{ techSpecContent: string, acceptanceSpecContent?: string|null }} artifacts
|
|
86
86
|
* @param {object} settings
|
|
87
87
|
* @param {{ force?: boolean, forceReview?: boolean, steal?: boolean, config?: object, riskVerdict?: import('../../planning-risk.js').RiskVerdict }} [opts]
|
|
88
|
-
* @returns {Promise<{ epicId: number,
|
|
88
|
+
* @returns {Promise<{ epicId: number, techSpecPersisted: boolean, acceptanceTable: 'persisted'|'waived'|'none', checkpoint: object, planningRisk: import('../../planning-risk.js').PlanningRiskEnvelope, reviewRouting: import('../../plan-review-routing.js').ReviewRoutingEnvelope }>}
|
|
89
89
|
*/
|
|
90
90
|
export async function runSpecPhase(
|
|
91
91
|
epicId,
|
|
92
92
|
provider,
|
|
93
|
-
{
|
|
93
|
+
{ techSpecContent, acceptanceSpecContent = null },
|
|
94
94
|
settings = {},
|
|
95
95
|
{
|
|
96
96
|
force = false,
|
|
@@ -153,7 +153,7 @@ export async function runSpecPhase(
|
|
|
153
153
|
const planResult = await planEpic(
|
|
154
154
|
epicId,
|
|
155
155
|
provider,
|
|
156
|
-
{
|
|
156
|
+
{ techSpecContent, acceptanceSpecContent },
|
|
157
157
|
settings,
|
|
158
158
|
{
|
|
159
159
|
force,
|
|
@@ -163,15 +163,13 @@ export async function runSpecPhase(
|
|
|
163
163
|
const specChanged = planResult?.persisted !== false;
|
|
164
164
|
|
|
165
165
|
const afterPlan = await provider.getEpic(epicId);
|
|
166
|
-
const prdId = afterPlan.linkedIssues?.prd ?? null;
|
|
167
|
-
const techSpecId = afterPlan.linkedIssues?.techSpec ?? null;
|
|
168
|
-
const acceptanceSpecId = afterPlan.linkedIssues?.acceptanceSpec ?? null;
|
|
169
166
|
|
|
170
167
|
// Story #2635 — cross-validate the authored Tech Spec body against the
|
|
171
|
-
// base branch and surface any stale path-shaped references
|
|
172
|
-
// Spec
|
|
173
|
-
//
|
|
174
|
-
//
|
|
168
|
+
// base branch and surface any stale path-shaped references. Story #4324:
|
|
169
|
+
// with the Tech Spec folded into the Epic body, the advisory comment
|
|
170
|
+
// lands on the Epic itself. Non-blocking: a missing base ref, an
|
|
171
|
+
// unreadable temp directory, or a provider failure downgrades to a
|
|
172
|
+
// warning so Phase 7 never fails on the advisory check.
|
|
175
173
|
const baseBranchRef = settings?.baseBranch ?? 'main';
|
|
176
174
|
const tempRoot = path.resolve(
|
|
177
175
|
PROJECT_ROOT,
|
|
@@ -179,7 +177,6 @@ export async function runSpecPhase(
|
|
|
179
177
|
);
|
|
180
178
|
const freshness = await runSpecFreshnessCheck({
|
|
181
179
|
epicId,
|
|
182
|
-
techSpecId,
|
|
183
180
|
techSpecContent,
|
|
184
181
|
baseBranchRef,
|
|
185
182
|
tempRoot,
|
|
@@ -220,9 +217,8 @@ export async function runSpecPhase(
|
|
|
220
217
|
},
|
|
221
218
|
spec: {
|
|
222
219
|
...currentState.spec,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
acceptanceSpecId,
|
|
220
|
+
techSpecPersisted: planResult?.techSpecPersisted === true,
|
|
221
|
+
acceptanceTable: planResult?.acceptanceTable ?? 'none',
|
|
226
222
|
completedAt: new Date().toISOString(),
|
|
227
223
|
},
|
|
228
224
|
},
|
|
@@ -256,13 +252,17 @@ export async function runSpecPhase(
|
|
|
256
252
|
const cleanup = await cleanupPhaseTempFiles({ phase: 'spec', epicId });
|
|
257
253
|
|
|
258
254
|
const acceptanceSummary =
|
|
259
|
-
|
|
255
|
+
planResult?.acceptanceTable === 'persisted'
|
|
256
|
+
? ' + ## Acceptance Table section'
|
|
257
|
+
: planResult?.acceptanceTable === 'waived'
|
|
258
|
+
? ' (acceptance waived)'
|
|
259
|
+
: '';
|
|
260
260
|
const freshnessSummary =
|
|
261
261
|
freshness.stale > 0 || freshness.ambiguous > 0
|
|
262
262
|
? ` ⚠️ Spec freshness: ${freshness.stale} stale / ${freshness.ambiguous} ambiguous reference(s) — see ${freshness.reportPath ?? 'report'}.`
|
|
263
263
|
: '';
|
|
264
264
|
Logger.info(
|
|
265
|
-
`[epic-plan-spec] ✅ Spec phase complete for Epic #${epicId}.
|
|
265
|
+
`[epic-plan-spec] ✅ Spec phase complete for Epic #${epicId}. Tech Spec sections folded into the Epic body${acceptanceSummary}.${freshnessSummary}`,
|
|
266
266
|
);
|
|
267
267
|
if (cleanup.deleted.length > 0) {
|
|
268
268
|
Logger.info(
|
|
@@ -272,9 +272,8 @@ export async function runSpecPhase(
|
|
|
272
272
|
|
|
273
273
|
return {
|
|
274
274
|
epicId,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
acceptanceSpecId,
|
|
275
|
+
techSpecPersisted: planResult?.techSpecPersisted === true,
|
|
276
|
+
acceptanceTable: planResult?.acceptanceTable ?? 'none',
|
|
278
277
|
checkpoint,
|
|
279
278
|
cleanup,
|
|
280
279
|
freshness,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* phases/spec-authoring-grounding.js — F10 spec-authoring code-grounding.
|
|
3
3
|
*
|
|
4
|
-
* Story #4139 (Epic #4131). `/plan` Phase 7 authors the
|
|
4
|
+
* Story #4139 (Epic #4131). `/plan` Phase 7 authors the Tech Spec
|
|
5
5
|
* from the Epic body, the scraped project docs, and the
|
|
6
6
|
* `codebaseSnapshot` structural view of the consumer repo. Two failure
|
|
7
7
|
* modes made that grounding silently partial:
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
* phases/spec-freshness.js — Tech Spec freshness check phase.
|
|
3
3
|
*
|
|
4
4
|
* Cross-validates the authored Tech Spec body against the base branch and
|
|
5
|
-
* surfaces any stale path-shaped references on the
|
|
6
|
-
*
|
|
5
|
+
* surfaces any stale path-shaped references on the Epic (Story #4324: the
|
|
6
|
+
* Tech Spec is folded into the Epic body, so the Epic is the comment
|
|
7
|
+
* target). Two side effects, both best-effort and non-blocking:
|
|
7
8
|
*
|
|
8
9
|
* 1. Write the full `{ stale, fresh, ambiguous }` report to
|
|
9
10
|
* `<tempRoot>/epic-<id>-spec-freshness.json` so downstream tooling
|
|
10
11
|
* (the `--code-freshness` health check on the roadmap, or an
|
|
11
12
|
* operator inspecting the run) can read it without re-probing git.
|
|
12
13
|
* 2. When `stale.length > 0`, upsert a `spec-freshness` structured
|
|
13
|
-
* comment on the
|
|
14
|
+
* comment on the Epic listing each suspect citation.
|
|
14
15
|
* The comment is upserted, so re-running spec re-renders the same
|
|
15
16
|
* comment in place rather than spamming the issue.
|
|
16
17
|
*
|
|
@@ -32,7 +33,6 @@ import { upsertStructuredComment } from '../../ticketing.js';
|
|
|
32
33
|
/**
|
|
33
34
|
* @param {object} opts
|
|
34
35
|
* @param {number} opts.epicId
|
|
35
|
-
* @param {number|null} opts.techSpecId
|
|
36
36
|
* @param {string} opts.techSpecContent
|
|
37
37
|
* @param {string} opts.baseBranchRef
|
|
38
38
|
* @param {string} opts.tempRoot
|
|
@@ -44,7 +44,6 @@ import { upsertStructuredComment } from '../../ticketing.js';
|
|
|
44
44
|
*/
|
|
45
45
|
export async function runSpecFreshnessCheck({
|
|
46
46
|
epicId,
|
|
47
|
-
techSpecId,
|
|
48
47
|
techSpecContent,
|
|
49
48
|
baseBranchRef,
|
|
50
49
|
tempRoot,
|
|
@@ -64,7 +63,6 @@ export async function runSpecFreshnessCheck({
|
|
|
64
63
|
);
|
|
65
64
|
const payload = {
|
|
66
65
|
epicId,
|
|
67
|
-
techSpecId,
|
|
68
66
|
baseBranchRef,
|
|
69
67
|
generatedAt: new Date().toISOString(),
|
|
70
68
|
summary: {
|
|
@@ -77,13 +75,12 @@ export async function runSpecFreshnessCheck({
|
|
|
77
75
|
await fileWriter(reportPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
78
76
|
|
|
79
77
|
let commentPosted = false;
|
|
80
|
-
if (report.stale.length > 0 && Number.isFinite(
|
|
78
|
+
if (report.stale.length > 0 && Number.isFinite(epicId)) {
|
|
81
79
|
const body = renderSpecFreshnessComment(report, {
|
|
82
80
|
baseBranchRef,
|
|
83
|
-
techSpecId,
|
|
84
81
|
epicId,
|
|
85
82
|
});
|
|
86
|
-
await commentUpserter(provider,
|
|
83
|
+
await commentUpserter(provider, epicId, 'spec-freshness', body);
|
|
87
84
|
commentPosted = true;
|
|
88
85
|
}
|
|
89
86
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* "epicId": 349,
|
|
19
19
|
* "startedAt": "...",
|
|
20
20
|
* "lastUpdatedAt": "...",
|
|
21
|
-
* "spec": { "
|
|
21
|
+
* "spec": { "techSpecPersisted": false, "acceptanceTable": "none", "completedAt": null },
|
|
22
22
|
* "decompose": { "ticketCount": null, "completedAt": null },
|
|
23
23
|
* "planningRisk": { "overallLevel": "...", "requiresReview": true, "gateDecision": "..." },
|
|
24
24
|
* "reviewRouting": { "decision": "review-required|auto-proceed|operator-override-review", "requiresStop": true, "forceReviewApplied": false },
|
|
@@ -106,9 +106,8 @@ export async function initialize({ provider, epicId, seed = {} } = {}) {
|
|
|
106
106
|
epicId,
|
|
107
107
|
startedAt: now,
|
|
108
108
|
spec: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
acceptanceSpecId: null,
|
|
109
|
+
techSpecPersisted: false,
|
|
110
|
+
acceptanceTable: 'none',
|
|
112
111
|
completedAt: null,
|
|
113
112
|
},
|
|
114
113
|
decompose: { ticketCount: null, completedAt: null },
|
|
@@ -35,7 +35,7 @@ export async function discoverOpenStories({ epicId, provider }) {
|
|
|
35
35
|
for (const t of descendants) {
|
|
36
36
|
const labels = t.labels ?? [];
|
|
37
37
|
if (!labels.includes(TYPE_LABELS.STORY)) continue;
|
|
38
|
-
// Defense-in-depth: never enumerate a context spec ticket (
|
|
38
|
+
// Defense-in-depth: never enumerate a context spec ticket (Tech
|
|
39
39
|
// Spec / Acceptance Spec) as a deliverable Story even if it carries
|
|
40
40
|
// `type::story`. The `createTicket` factory no longer stamps context
|
|
41
41
|
// tickets that way, but a context ticket mislabelled out-of-band must
|
|
@@ -5,20 +5,19 @@
|
|
|
5
5
|
* Auto-close is now the default for `/deliver` (the human PR-merge is
|
|
6
6
|
* the gate); no per-Epic label snapshot is required.
|
|
7
7
|
*
|
|
8
|
-
* Acceptance-spec start gate (
|
|
9
|
-
* *either* the operator has explicitly waived the acceptance
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* sufficient
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* dispatch race ahead without a spec at all.
|
|
8
|
+
* Acceptance-spec start gate (Story #4324): an Epic may be delivered when
|
|
9
|
+
* *either* the operator has explicitly waived the acceptance requirement
|
|
10
|
+
* via the `acceptance::n-a` label, *or* the Epic body carries the
|
|
11
|
+
* `## Acceptance Table` managed section (the folded home of the retired
|
|
12
|
+
* `context::acceptance-spec` ticket's AC-ID table). Presence is
|
|
13
|
+
* sufficient — the reviewer's OK during /plan Phase 7 is the approval
|
|
14
|
+
* signal. This still refuses to launch Epics that skipped the /plan
|
|
15
|
+
* Phase 7 acceptance authoring step (or didn't waive), surfacing the gap
|
|
16
|
+
* at delivery time rather than letting Story dispatch race ahead without
|
|
17
|
+
* a spec at all.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
import {
|
|
20
|
+
import { hasEpicSection } from '../../../epic-body-sections.js';
|
|
22
21
|
import { ACCEPTANCE_NA } from '../../../label-constants.js';
|
|
23
22
|
import { discoverOpenStories } from './build-wave-dag.js';
|
|
24
23
|
|
|
@@ -82,29 +81,23 @@ async function discoverStoryIds({ epicId, provider }) {
|
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
/**
|
|
85
|
-
* Refuse to launch /deliver when the acceptance
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
84
|
+
* Refuse to launch /deliver when the acceptance precondition has not been
|
|
85
|
+
* satisfied. Throws a clear `Error` (per orchestration-error-handling
|
|
86
|
+
* rule) so the `runAsCli` boundary maps it to `process.exit(1)` with the
|
|
87
|
+
* operator-visible message intact.
|
|
89
88
|
*
|
|
90
|
-
* The gate
|
|
91
|
-
* deliberately ignored — closure is no longer required as the approval
|
|
92
|
-
* signal.
|
|
89
|
+
* The gate checks section presence (or the waiver label) only.
|
|
93
90
|
*
|
|
94
|
-
* @param {{ epic: { labels?: string[],
|
|
91
|
+
* @param {{ epic: { labels?: string[], body?: string }, epicId: number }} args
|
|
95
92
|
*/
|
|
96
93
|
function assertAcceptanceSpecGate({ epic, epicId }) {
|
|
97
94
|
const labels = epic?.labels ?? [];
|
|
98
95
|
if (labels.includes(ACCEPTANCE_NA)) return;
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
epic?.linkedIssues ?? parseLinkedIssues(epic?.body ?? '');
|
|
102
|
-
const acceptanceSpecId = linkedIssues?.acceptanceSpec ?? null;
|
|
103
|
-
|
|
104
|
-
if (!acceptanceSpecId) {
|
|
97
|
+
if (!hasEpicSection(epic?.body ?? '', 'acceptanceTable')) {
|
|
105
98
|
throw new Error(
|
|
106
|
-
`[epic-deliver] Epic #${epicId} cannot launch: no
|
|
107
|
-
'Run /plan Phase 7 to author
|
|
99
|
+
`[epic-deliver] Epic #${epicId} cannot launch: the Epic body has no ## Acceptance Table section and the acceptance::n-a waiver label is absent. ` +
|
|
100
|
+
'Run /plan Phase 7 to author the acceptance table, or apply the acceptance::n-a label to the Epic to opt out.',
|
|
108
101
|
);
|
|
109
102
|
}
|
|
110
103
|
}
|
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
* - Per-instance `Set<string>` of `${event}:${seqId}` keys. A repeat
|
|
24
24
|
* `(event, seqId)` short-circuits without re-running reconciliation
|
|
25
25
|
* and emits no events.
|
|
26
|
-
* - The reconciler helper
|
|
27
|
-
*
|
|
26
|
+
* - The reconciler helper's only write is the section-scoped
|
|
27
|
+
* disposition record inside the Epic body's ## Acceptance Table
|
|
28
|
+
* region (Story #4324) — itself idempotent — so the seqId guard is
|
|
29
|
+
* the only defence the listener needs.
|
|
28
30
|
*
|
|
29
31
|
* Side-effect firewall: the listener owns the reconciliation call and
|
|
30
32
|
* the four outcome emits. It does NOT mutate labels, post comments, or
|
|
@@ -233,10 +235,14 @@ export class AcceptanceReconciler {
|
|
|
233
235
|
loggerImpl: this.logger,
|
|
234
236
|
// Honour `acceptance::n-a` — the helper already returns
|
|
235
237
|
// `status: 'waived'` for that label, but `skipWhenWaived: true`
|
|
236
|
-
// additionally tolerates a missing
|
|
237
|
-
// (we want the same forgiving stance the inline
|
|
238
|
-
// had).
|
|
238
|
+
// additionally tolerates a missing acceptance-table section under
|
|
239
|
+
// the waiver (we want the same forgiving stance the inline
|
|
240
|
+
// finalize call had).
|
|
239
241
|
skipWhenWaived: true,
|
|
242
|
+
// Close time is the one place the verification outcome is
|
|
243
|
+
// recorded into the Epic body's ## Acceptance Table section
|
|
244
|
+
// (section-scoped write, Story #4324).
|
|
245
|
+
writeDispositions: true,
|
|
240
246
|
});
|
|
241
247
|
} catch (err) {
|
|
242
248
|
thrown = err;
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
* 4. Otherwise, invoke `runFinalizeFn`. The production default
|
|
25
25
|
* (`composeBusOwnedFinalize`) chains
|
|
26
26
|
* a. `openOrLocatePr({ epicId, headBranch, baseBranch })`
|
|
27
|
-
* b. `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* b. `postHandoffComment({ epicId, prNumber, prUrl, provider })`
|
|
28
|
+
* and returns `{ prNumber, prUrl, handoff }`. (Story #4324 retired
|
|
29
|
+
* the `closePlanningTickets` sweep with the context-ticket classes —
|
|
30
|
+
* there are no planning tickets to close.)
|
|
30
31
|
* 5. Emit `pr.created` then `epic.finalize.end`.
|
|
31
32
|
*
|
|
32
33
|
* Why the Finalizer does NOT emit `epic.merge.ready` (Story #3367):
|
|
@@ -65,31 +66,27 @@ import {
|
|
|
65
66
|
} from '../../../feedback-loop/audit-results-graduator.js';
|
|
66
67
|
import { graduateFindings as defaultGraduateFindings } from '../../../feedback-loop/code-review-graduator.js';
|
|
67
68
|
import { parsePrNumberFromUrl } from '../../../github-url.js';
|
|
68
|
-
import { closePlanningTickets as defaultClosePlanningTickets } from '../../finalize/close-planning-tickets.js';
|
|
69
69
|
import { openOrLocatePr as defaultOpenOrLocatePr } from '../../finalize/open-or-locate-pr.js';
|
|
70
70
|
import { postHandoffComment as defaultPostHandoffComment } from '../../finalize/post-handoff-comment.js';
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Build the production default `runFinalizeFn` that composes the
|
|
73
|
+
* Build the production default `runFinalizeFn` that composes the
|
|
74
74
|
* bus-owned finalize helpers. Exported as a factory so the listener can
|
|
75
75
|
* inject the provider (which the default needs but tests inject by
|
|
76
76
|
* supplying their own `runFinalizeFn` outright).
|
|
77
77
|
*
|
|
78
|
-
* Returns `{ prNumber, prUrl,
|
|
78
|
+
* Returns `{ prNumber, prUrl, handoff }` on success, or
|
|
79
79
|
* `{ blocker: { reason, detail } }` when a step fails with an
|
|
80
80
|
* unrecoverable error that should keep the Epic at `agent::blocked`.
|
|
81
81
|
*
|
|
82
82
|
* @param {{
|
|
83
83
|
* provider?: object|null,
|
|
84
84
|
* openOrLocatePrFn?: typeof defaultOpenOrLocatePr,
|
|
85
|
-
* closePlanningTicketsFn?: typeof defaultClosePlanningTickets,
|
|
86
85
|
* postHandoffCommentFn?: typeof defaultPostHandoffComment,
|
|
87
86
|
* }} deps
|
|
88
87
|
*/
|
|
89
88
|
export function composeBusOwnedFinalize(deps = {}) {
|
|
90
89
|
const openOrLocatePrFn = deps.openOrLocatePrFn ?? defaultOpenOrLocatePr;
|
|
91
|
-
const closePlanningTicketsFn =
|
|
92
|
-
deps.closePlanningTicketsFn ?? defaultClosePlanningTickets;
|
|
93
90
|
const postHandoffCommentFn =
|
|
94
91
|
deps.postHandoffCommentFn ?? defaultPostHandoffComment;
|
|
95
92
|
const provider = deps.provider ?? null;
|
|
@@ -132,28 +129,10 @@ export function composeBusOwnedFinalize(deps = {}) {
|
|
|
132
129
|
};
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// arm auto-merge).
|
|
140
|
-
let planningClose = null;
|
|
141
|
-
if (provider) {
|
|
142
|
-
try {
|
|
143
|
-
planningClose = await closePlanningTicketsFn({
|
|
144
|
-
epicId,
|
|
145
|
-
provider,
|
|
146
|
-
});
|
|
147
|
-
} catch (err) {
|
|
148
|
-
return {
|
|
149
|
-
blocker: {
|
|
150
|
-
reason: 'close-planning-tickets-failed',
|
|
151
|
-
detail: err?.message ?? String(err),
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
132
|
+
// The handoff comment requires a provider. The lifecycle-emit CLI
|
|
133
|
+
// may construct the Finalizer without one; in that case the step
|
|
134
|
+
// short-circuits (the run itself still succeeds — the PR is open and
|
|
135
|
+
// the bus can arm auto-merge).
|
|
157
136
|
let handoff = null;
|
|
158
137
|
if (provider) {
|
|
159
138
|
try {
|
|
@@ -180,7 +159,6 @@ export function composeBusOwnedFinalize(deps = {}) {
|
|
|
180
159
|
prNumber: openResult.prNumber,
|
|
181
160
|
prUrl: openResult.url,
|
|
182
161
|
created: openResult.created,
|
|
183
|
-
planningClose,
|
|
184
162
|
handoff,
|
|
185
163
|
};
|
|
186
164
|
};
|
|
@@ -252,8 +230,8 @@ export class Finalizer {
|
|
|
252
230
|
* @param {Function} [opts.ghPrListHeadFn] override of the
|
|
253
231
|
* idempotency probe.
|
|
254
232
|
* @param {object} [opts.provider] Ticketing provider forwarded to
|
|
255
|
-
* the default `runFinalizeFn` (for `
|
|
256
|
-
*
|
|
233
|
+
* the default `runFinalizeFn` (for `postHandoffComment`) and to the
|
|
234
|
+
* graduators.
|
|
257
235
|
* @param {object} [opts.config] Resolved agent config; forwarded to
|
|
258
236
|
* the graduators.
|
|
259
237
|
* @param {{owner:string,repo:string}} [opts.currentRepo]
|
|
@@ -283,11 +261,9 @@ export class Finalizer {
|
|
|
283
261
|
composeBusOwnedFinalize({ provider: this.provider });
|
|
284
262
|
this.ghPrListHeadFn = opts.ghPrListHeadFn ?? ghPrListHead;
|
|
285
263
|
// ultrareview bug_007: the existing-PR short-circuit must run the
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
this.closePlanningTicketsFn =
|
|
290
|
-
opts.closePlanningTicketsFn ?? defaultClosePlanningTickets;
|
|
264
|
+
// handoff-comment upsert (idempotent) so crash-recovery replays
|
|
265
|
+
// don't skip it. Stored as an instance override so tests can swap
|
|
266
|
+
// it with a no-op stub.
|
|
291
267
|
this.postHandoffCommentFn =
|
|
292
268
|
opts.postHandoffCommentFn ?? defaultPostHandoffComment;
|
|
293
269
|
this.config = opts.config ?? null;
|
|
@@ -375,24 +351,11 @@ export class Finalizer {
|
|
|
375
351
|
);
|
|
376
352
|
const prNumber = parsePrNumberFromUrl(existingUrl);
|
|
377
353
|
// ultrareview bug_007: even on the short-circuit path we MUST
|
|
378
|
-
// run the
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// three planning context tickets open and never posts the
|
|
384
|
-
// handoff comment, even though epic.merge.ready is still emitted.
|
|
385
|
-
try {
|
|
386
|
-
await this.closePlanningTicketsFn({
|
|
387
|
-
epicId: this.epicId,
|
|
388
|
-
provider: this.provider,
|
|
389
|
-
cwd: this.cwd,
|
|
390
|
-
});
|
|
391
|
-
} catch (err) {
|
|
392
|
-
this.logger.warn?.(
|
|
393
|
-
`[Finalizer] closePlanningTickets on short-circuit failed (swallowed; replay will retry): ${err?.message ?? err}`,
|
|
394
|
-
);
|
|
395
|
-
}
|
|
354
|
+
// run the handoff-comment upsert. The helper is idempotent —
|
|
355
|
+
// postHandoffComment edits an existing marker comment in place.
|
|
356
|
+
// Without this call, a crash-recovery replay against an
|
|
357
|
+
// already-open PR never posts the handoff comment, even though
|
|
358
|
+
// epic.merge.ready is still emitted.
|
|
396
359
|
try {
|
|
397
360
|
await this.postHandoffCommentFn({
|
|
398
361
|
epicId: this.epicId,
|
|
@@ -425,8 +388,8 @@ export class Finalizer {
|
|
|
425
388
|
|
|
426
389
|
// 3. Run the composed bus-owned finalize (or the test-injected
|
|
427
390
|
// `runFinalizeFn`). The default chains openOrLocatePr →
|
|
428
|
-
//
|
|
429
|
-
//
|
|
391
|
+
// postHandoffComment in order and returns
|
|
392
|
+
// `{ prNumber, prUrl, handoff }`.
|
|
430
393
|
let finalize;
|
|
431
394
|
try {
|
|
432
395
|
finalize = await this.runFinalizeFn({
|
|
@@ -183,7 +183,7 @@ export async function buildDefaultListenerChain(opts = {}) {
|
|
|
183
183
|
// 3. Finalizer — opens the PR on acceptance.reconcile.ok or .waived
|
|
184
184
|
// (Story #2893 split waiver out of .skipped) via the bus-owned
|
|
185
185
|
// default (`composeBusOwnedFinalize`, Story #2894), which chains
|
|
186
|
-
// openOrLocatePr →
|
|
186
|
+
// openOrLocatePr → postHandoffComment and
|
|
187
187
|
// emits `epic.merge.ready` on success. The legacy
|
|
188
188
|
// `d1-default-no-op` blocker is gone; the listener constructed
|
|
189
189
|
// with no `runFinalizeFn` override always runs the real flow.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bounded planning-context budget for the `--emit-context` planning scripts
|
|
5
5
|
* (Epic #817 Story 9). Provides a summary representation of large markdown
|
|
6
|
-
* payloads (project docs,
|
|
6
|
+
* payloads (project docs, Epic body, Tech Spec body) so the host LLM is not
|
|
7
7
|
* handed multi-100KB envelopes when the underlying material exceeds the
|
|
8
8
|
* configured `maxBytes` budget.
|
|
9
9
|
*
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* call (`getTicket(epicId)`) used to walk the hierarchy. The hash
|
|
23
23
|
* covers the Epic's id, body, labels (sorted), and (when present)
|
|
24
24
|
* `updatedAt` — the same fields that drive Story discovery and the
|
|
25
|
-
* acceptance
|
|
25
|
+
* acceptance start gate (the Epic body's ## Acceptance Table).
|
|
26
26
|
* - Cache miss is signalled by `readCache` returning `null`. Callers
|
|
27
27
|
* MUST fall back to a fresh snapshot/DAG pass.
|
|
28
28
|
* - `writeCache` creates the parent directory recursively and writes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* spec-freshness.js — Tech Spec post-author cross-validation against the
|
|
3
3
|
* current codebase.
|
|
4
4
|
*
|
|
5
|
-
* `/plan` Phase 7 authors
|
|
5
|
+
* `/plan` Phase 7 authors the Tech Spec from documentation alone.
|
|
6
6
|
* When `project.docsContextFiles` (architecture.md, etc.) drift from
|
|
7
7
|
* the real source tree, the Architect persona happily cites modules and
|
|
8
8
|
* paths that no longer exist. The mismatch only surfaces at delivery time,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* probes each against `baseBranchRef` (via `git cat-file -e`), and returns a
|
|
13
13
|
* `{ stale, fresh, ambiguous }` envelope. The caller (epic-plan-spec.js)
|
|
14
14
|
* uses the result to write a JSON report and post an advisory structured
|
|
15
|
-
* comment on the
|
|
15
|
+
* comment on the Epic. The check is intentionally non-blocking —
|
|
16
16
|
* planning continues even when stale references are present, because the
|
|
17
17
|
* operator may legitimately be planning to create the path the Tech Spec
|
|
18
18
|
* cites.
|
|
@@ -274,7 +274,7 @@ export function validateSpecFreshness(specBody, opts = {}) {
|
|
|
274
274
|
* exact rendering contract.
|
|
275
275
|
*
|
|
276
276
|
* @param {{ stale: Array, ambiguous: Array, fresh: Array }} report
|
|
277
|
-
* @param {{ baseBranchRef: string,
|
|
277
|
+
* @param {{ baseBranchRef: string, epicId?: number }} ctx
|
|
278
278
|
* @returns {string}
|
|
279
279
|
*/
|
|
280
280
|
export function renderSpecFreshnessComment(report, ctx) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* spec-section-validator.js — Phase 7.5 Tech Spec post-authoring section gate.
|
|
3
3
|
*
|
|
4
|
-
* `/plan` Phase 7 authors a Tech Spec from documentation and the
|
|
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
|
|
6
6
|
* against the Tech Spec's `## Delivery Slicing` section, which the
|
|
7
7
|
* decompose-author skill uses as the capability-boundary anchor. When the
|
package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js
CHANGED
|
@@ -32,6 +32,7 @@ import { Logger as DefaultLogger } from '../../../../Logger.js';
|
|
|
32
32
|
import {
|
|
33
33
|
calculateAll as defaultCalculateAll,
|
|
34
34
|
scanDirectory as defaultScanDirectory,
|
|
35
|
+
isIgnoredByGlobs as isIgnoredByGlobsMi,
|
|
35
36
|
} from '../../../../maintainability-utils.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -101,7 +102,20 @@ export function buildKindScorer({
|
|
|
101
102
|
const underTarget = targetAbsDirs.some(
|
|
102
103
|
(root) => abs === root || abs.startsWith(`${root}${path.sep}`),
|
|
103
104
|
);
|
|
104
|
-
|
|
105
|
+
// Apply `ignoreGlobs` here too — the full-scope walk drops
|
|
106
|
+
// ignore-matched files via `scanDirectory`, so the diff-scope path
|
|
107
|
+
// must do the same or an ignored-but-changed file (e.g. one matched
|
|
108
|
+
// by `config-settings-schema*.js`) enters `rows` and drags the
|
|
109
|
+
// `rollup["*"].min` below the maintainability floor. This mirrors the
|
|
110
|
+
// canonical `buildDefaultMaintainabilityScorer` (refresh-service.js,
|
|
111
|
+
// Story #4293); the story-close auto-refresh routes through this
|
|
112
|
+
// scorer, not that one, so the fix has to live here too.
|
|
113
|
+
if (
|
|
114
|
+
underTarget &&
|
|
115
|
+
!isIgnoredByGlobsMi(abs, miIgnoreGlobs, effectiveCwd)
|
|
116
|
+
) {
|
|
117
|
+
sourceList.push(abs);
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
const scores = await calculateAll(sourceList);
|