mandrel 1.80.0 → 1.82.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 +5 -5
- package/.agents/docs/SDLC.md +93 -80
- package/.agents/docs/configuration.md +11 -1
- package/.agents/docs/quality-gates.md +61 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +9 -7
- 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 +10 -0
- package/.agents/scripts/acceptance-spec-reconciler.js +143 -59
- package/.agents/scripts/epic-deliver-prepare.js +0 -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/baselines/env-overrides.js +35 -0
- 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/temp-paths.js +1 -4
- package/.agents/scripts/lib/config-settings-schema.js +5 -0
- package/.agents/scripts/lib/epic-body-sections.js +222 -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/label-constants.js +7 -17
- package/.agents/scripts/lib/label-taxonomy.js +4 -21
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +47 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/pipeline.js +1 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +2 -1
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +19 -8
- 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/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/qa-context-hydrator.js +6 -85
- 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 +37 -18
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -18
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +7 -6
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +161 -109
- 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 +6 -6
- 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.md +32 -44
- package/.agents/workflows/helpers/epic-audit.md +11 -8
- package/.agents/workflows/helpers/epic-deliver-story.md +10 -16
- 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.md +114 -99
- package/.agents/workflows/helpers/single-story-deliver.md +1 -1
- 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/docs/CHANGELOG.md +28 -0
- package/package.json +1 -1
- 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
|
@@ -86,6 +86,31 @@ function resolveAcceptance(story) {
|
|
|
86
86
|
return Array.isArray(body?.acceptance) ? body.acceptance : [];
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the `depends_on` edge list for a Story. `depends_on` may live at the
|
|
91
|
+
* top level (the validator normalizes it there — see `ticket-validator.js`) or
|
|
92
|
+
* inside the structured body (the shape `story-body.js` parses out). Prefer the
|
|
93
|
+
* top-level array, falling back to the resolved body's `depends_on`. Returns
|
|
94
|
+
* only non-empty string slugs — mirroring the `story-body.js` normalization so
|
|
95
|
+
* a blank / non-string edge never counts as a real dependency.
|
|
96
|
+
*
|
|
97
|
+
* The cross-Story validator already rejects any `depends_on` slug that does not
|
|
98
|
+
* match a sibling in the same decomposition, so every edge that survives to the
|
|
99
|
+
* sizing layer is guaranteed to point at a sibling. The merge-candidate
|
|
100
|
+
* heuristic therefore only needs to know whether the list is non-empty.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} story
|
|
103
|
+
* @returns {string[]}
|
|
104
|
+
*/
|
|
105
|
+
function resolveDependsOn(story) {
|
|
106
|
+
const raw = Array.isArray(story?.depends_on)
|
|
107
|
+
? story.depends_on
|
|
108
|
+
: (resolveStoryBody(story)?.depends_on ?? []);
|
|
109
|
+
return Array.isArray(raw)
|
|
110
|
+
? raw.filter((d) => typeof d === 'string' && d.trim().length > 0)
|
|
111
|
+
: [];
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
export const DEFAULT_TASK_SIZING = Object.freeze({
|
|
90
115
|
// Typical-Story warning thresholds (soft — emit advisory findings).
|
|
91
116
|
// Story #4162 raised `softFiles` 8 → 15: a capability-sized Story routinely
|
|
@@ -97,6 +122,15 @@ export const DEFAULT_TASK_SIZING = Object.freeze({
|
|
|
97
122
|
// Hard ceilings (rejection unless lifted).
|
|
98
123
|
hardFiles: 30,
|
|
99
124
|
maxAcceptance: 14,
|
|
125
|
+
// Under-size (merge-candidate) thresholds (Story #4312). A Story with a
|
|
126
|
+
// footprint at or below BOTH ceilings that also carries at least one
|
|
127
|
+
// `depends_on` edge to a sibling looks like a dependent fragment rather than
|
|
128
|
+
// a capability slice — the machine-checkable form of the single-consumer
|
|
129
|
+
// merge rule. Emitted as a `soft` advisory only; never a rejection. A tiny
|
|
130
|
+
// ORPHAN Story (no `depends_on`) stays silent — small orthogonal slices are
|
|
131
|
+
// legitimate.
|
|
132
|
+
mergeCandidateMaxFiles: 3,
|
|
133
|
+
mergeCandidateMaxAcceptance: 4,
|
|
100
134
|
});
|
|
101
135
|
|
|
102
136
|
/**
|
|
@@ -117,6 +151,16 @@ export const DEFAULT_TASK_SIZING = Object.freeze({
|
|
|
117
151
|
* as a single PR — not a single module or file. Module-level slices fold into
|
|
118
152
|
* the capability they belong to, and a Story whose only consumer is one
|
|
119
153
|
* sibling Story is merged into that sibling (single-consumer merge rule).
|
|
154
|
+
*
|
|
155
|
+
* The `envelopeFloor` sentence is the **soft** complement to the sizing
|
|
156
|
+
* section's envelope framing (Story #4313): the delivery envelope
|
|
157
|
+
* (`maxTokenBudget`) bounds the TOP of a Story, but under-utilizing it is
|
|
158
|
+
* itself a merge signal. It is deliberately prose-only — an illustrative
|
|
159
|
+
* fraction, not a threshold constant, and no validator finding backs it (the
|
|
160
|
+
* mechanical backstop is the `merge-candidate` finding). It names the
|
|
161
|
+
* per-Story delivery-session cost (hydration, branch, PR, review, CI) so the
|
|
162
|
+
* "models one-shot bigger things now" planning assumption is stated, not
|
|
163
|
+
* implicit.
|
|
120
164
|
*/
|
|
121
165
|
export const DELIVERABLE_GRANULARITY_GUIDANCE = Object.freeze({
|
|
122
166
|
// The one-sentence definition of Story granularity.
|
|
@@ -125,6 +169,9 @@ export const DELIVERABLE_GRANULARITY_GUIDANCE = Object.freeze({
|
|
|
125
169
|
// The single-consumer merge rule.
|
|
126
170
|
singleConsumerRule:
|
|
127
171
|
'**Single-consumer merge rule.** A Story whose only consumer is one sibling Story should be **merged into that sibling** rather than emitted separately — a single-consumer downstream slice is not its own unit of work.',
|
|
172
|
+
// The envelope-floor heuristic (soft; no validator finding — Story #4313).
|
|
173
|
+
envelopeFloor:
|
|
174
|
+
'**Envelope floor — under-utilizing the envelope is a merge signal.** A Story that would plausibly consume well under a third of the delivery envelope (`maxTokenBudget`) and is neither parallel-deliverable nor orthogonal to its siblings should be **merged into its consumer**. The fraction is illustrative, not a threshold — the point is that modern frontier models one-shot capability-sized changes, so a chain of small dependent Stories needlessly pays a full delivery session (hydration, branch, PR, review, CI) per link. Merge such links up unless a parallelism or orthogonality reason justifies the separate slice.',
|
|
128
175
|
});
|
|
129
176
|
|
|
130
177
|
/**
|
|
@@ -291,6 +338,66 @@ function computeMissingReasonToExistFinding(story) {
|
|
|
291
338
|
return hasReason ? [] : [makeMissingReasonToExist(story.slug)];
|
|
292
339
|
}
|
|
293
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Soft, advisory `merge-candidate` finding (Story #4312). Surfaces a Story that
|
|
343
|
+
* looks like a dependent fragment rather than a capability slice — the
|
|
344
|
+
* machine-checkable form of the single-consumer merge rule
|
|
345
|
+
* (`DELIVERABLE_GRANULARITY_GUIDANCE.singleConsumerRule`). Never a rejection:
|
|
346
|
+
* `severity: 'soft'`, so it rides the advisory `findings[]` channel and never
|
|
347
|
+
* enters the hard `errors[]` array.
|
|
348
|
+
*
|
|
349
|
+
* The rendered message names the depended-on sibling slug(s) and recommends
|
|
350
|
+
* merging into the consumer, mirroring the tone of the `wide-undeclared` nudge.
|
|
351
|
+
*/
|
|
352
|
+
function makeMergeCandidate(slug, fileCount, acceptanceCount, dependsOn) {
|
|
353
|
+
const siblings = dependsOn.map((d) => `"${d}"`).join(', ');
|
|
354
|
+
return {
|
|
355
|
+
kind: 'merge-candidate',
|
|
356
|
+
severity: 'soft',
|
|
357
|
+
ticketSlug: slug,
|
|
358
|
+
fileCount,
|
|
359
|
+
acceptanceCount,
|
|
360
|
+
dependsOn,
|
|
361
|
+
message: `Story "${slug}" is a thin dependent slice (${fileCount} declared file(s), ${acceptanceCount} acceptance item(s)) that depends on sibling(s) ${siblings}. A Story whose only role is to feed one sibling is not its own unit of work — consider merging it into the consumer (single-consumer merge rule) rather than shipping it as a separate slice.`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Emit a soft `merge-candidate` finding when a Story meets the under-size
|
|
367
|
+
* heuristic (Story #4312): footprint ≤ `mergeCandidateMaxFiles` declared
|
|
368
|
+
* `changes[]` files AND ≤ `mergeCandidateMaxAcceptance` acceptance items AND at
|
|
369
|
+
* least one `depends_on` edge to a sibling. All three conditions MUST hold — a
|
|
370
|
+
* tiny ORPHAN Story (no `depends_on`) stays silent because small orthogonal
|
|
371
|
+
* slices are legitimate. Glob entries mark the footprint as unknown-width, so a
|
|
372
|
+
* glob-carrying Story is never a merge candidate.
|
|
373
|
+
*
|
|
374
|
+
* @param {object} story
|
|
375
|
+
* @param {{ fileCount: number, hasGlobs: boolean }} changesAnalysis
|
|
376
|
+
* @param {number} acceptanceCount
|
|
377
|
+
* @param {object} sizing
|
|
378
|
+
* @returns {object[]}
|
|
379
|
+
*/
|
|
380
|
+
function computeMergeCandidateFinding(
|
|
381
|
+
story,
|
|
382
|
+
changesAnalysis,
|
|
383
|
+
acceptanceCount,
|
|
384
|
+
sizing,
|
|
385
|
+
) {
|
|
386
|
+
if (changesAnalysis.hasGlobs) return [];
|
|
387
|
+
const dependsOn = resolveDependsOn(story);
|
|
388
|
+
if (dependsOn.length === 0) return [];
|
|
389
|
+
if (changesAnalysis.fileCount > sizing.mergeCandidateMaxFiles) return [];
|
|
390
|
+
if (acceptanceCount > sizing.mergeCandidateMaxAcceptance) return [];
|
|
391
|
+
return [
|
|
392
|
+
makeMergeCandidate(
|
|
393
|
+
story.slug,
|
|
394
|
+
changesAnalysis.fileCount,
|
|
395
|
+
acceptanceCount,
|
|
396
|
+
dependsOn,
|
|
397
|
+
),
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
|
|
294
401
|
/**
|
|
295
402
|
* Returns true when a `changes[]` entry is a glob pattern. Handles both the
|
|
296
403
|
* canonical PathEntry object form `{ path, assumption }` and legacy strings.
|
|
@@ -394,6 +501,7 @@ function computeStorySizingFindings(story, sizing) {
|
|
|
394
501
|
const acceptance = resolveAcceptance(story);
|
|
395
502
|
const changes = Array.isArray(body?.changes) ? body.changes : [];
|
|
396
503
|
const declaredWide = isDeclaredWide(body?.wide ?? null);
|
|
504
|
+
const changesAnalysis = analyseChanges(changes);
|
|
397
505
|
|
|
398
506
|
// Soft, advisory: flag acceptance criteria that reference a configuration
|
|
399
507
|
// constant without inlining a concrete value (Story #3855). Independent of
|
|
@@ -407,6 +515,19 @@ function computeStorySizingFindings(story, sizing) {
|
|
|
407
515
|
// Independent of the numeric sizing layers below.
|
|
408
516
|
out.push(...computeMissingReasonToExistFinding(story));
|
|
409
517
|
|
|
518
|
+
// Soft, advisory: flag a thin dependent slice (Story #4312) — the symmetric
|
|
519
|
+
// under-size backstop to the over-size ceilings. A tiny footprint with a
|
|
520
|
+
// `depends_on` edge to a sibling is a merge candidate under the
|
|
521
|
+
// single-consumer merge rule. Independent of the numeric sizing layers below.
|
|
522
|
+
out.push(
|
|
523
|
+
...computeMergeCandidateFinding(
|
|
524
|
+
story,
|
|
525
|
+
changesAnalysis,
|
|
526
|
+
acceptance.length,
|
|
527
|
+
sizing,
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
|
|
410
531
|
// Acceptance ceiling + soft warn.
|
|
411
532
|
if (acceptance.length > sizing.maxAcceptance) {
|
|
412
533
|
out.push(
|
|
@@ -428,7 +549,7 @@ function computeStorySizingFindings(story, sizing) {
|
|
|
428
549
|
);
|
|
429
550
|
}
|
|
430
551
|
|
|
431
|
-
const { fileCount, hasGlobs } =
|
|
552
|
+
const { fileCount, hasGlobs } = changesAnalysis;
|
|
432
553
|
|
|
433
554
|
// Glob entries mark the Story as unknown-width: a glob cannot be bounded by
|
|
434
555
|
// the numeric ceiling, so skip it. A non-wide Story carrying globs gets an
|
|
@@ -295,15 +295,12 @@ async function processCascadeParentLocked(
|
|
|
295
295
|
// PR-driven `Closes #N` auto-close, and a recovery transition in
|
|
296
296
|
// `epic-deliver-finalize.js`).
|
|
297
297
|
//
|
|
298
|
-
//
|
|
299
|
-
// cascade
|
|
300
|
-
// the assumption that the operator would close them manually
|
|
301
|
-
// post-merge — but that step never reliably happened and leaving
|
|
298
|
+
// Legacy planning tickets (pre-#4324 `context::*` artifacts on
|
|
299
|
+
// historical Epics) DO close via cascade (Story #1951): leaving
|
|
302
300
|
// them open as native sub-issues of the Epic blocks GitHub from
|
|
303
|
-
// honoring the Epic's `Closes #N` footer.
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// planning ticket directly.
|
|
301
|
+
// honoring the Epic's `Closes #N` footer. New Epics carry their
|
|
302
|
+
// planning content on the Epic body itself, so this branch is
|
|
303
|
+
// purely a legacy-hygiene path.
|
|
307
304
|
//
|
|
308
305
|
// Reuse the parentSnapshot from the idempotency check above — it is
|
|
309
306
|
// a fresh read (cache was invalidated before the getTicket call) and
|
|
@@ -93,7 +93,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
93
93
|
// a sharpened body rewrite, recording the persistence event for audit.
|
|
94
94
|
'clarity-gate-update',
|
|
95
95
|
// Story #2635 — Phase 7 Tech Spec freshness check. `epic-plan-spec.js`
|
|
96
|
-
// upserts a `spec-freshness` comment on the
|
|
96
|
+
// upserts a `spec-freshness` comment on the Epic listing any
|
|
97
97
|
// path-shaped references that don't exist at the base branch, so the
|
|
98
98
|
// operator can correct drift before Phase 8 decomposes from a stale
|
|
99
99
|
// spec. Advisory: the run continues regardless of the report contents.
|
|
@@ -118,7 +118,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
118
118
|
'model-attribution',
|
|
119
119
|
// Story #2894 — `finalize/post-handoff-comment.js` upserts an
|
|
120
120
|
// `epic-handoff` comment on the Epic at the end of the bus-owned
|
|
121
|
-
// finalize flow (after `open-or-locate-pr`
|
|
121
|
+
// finalize flow (after `open-or-locate-pr`
|
|
122
122
|
// succeed). The marker carries the freshly opened/located PR URL and
|
|
123
123
|
// its number so operators can jump straight to the PR from the Epic
|
|
124
124
|
// ticket. Re-invocations upsert the same marker rather than appending
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The spec and decompose phases write several Epic-scoped temp files under
|
|
5
5
|
* the per-Epic tree (`temp/epic-<id>/planner-context.json`,
|
|
6
|
-
* `temp/epic-<id>/
|
|
6
|
+
* `temp/epic-<id>/techspec.md`, etc. — see `lib/config/temp-paths.js`). The
|
|
7
7
|
* workflow .md previously told the operator to `Remove-Item` those files by
|
|
8
8
|
* name at the end of each phase, which rots: adding a new temp file in the
|
|
9
9
|
* script required a synchronized markdown edit, and missed edits left
|
|
@@ -37,7 +37,6 @@ import { PROJECT_ROOT, resolveConfig } from './config-resolver.js';
|
|
|
37
37
|
export const PHASE_TEMP_BASENAMES = Object.freeze({
|
|
38
38
|
spec: Object.freeze([
|
|
39
39
|
'planner-context.json',
|
|
40
|
-
'prd.md',
|
|
41
40
|
'techspec.md',
|
|
42
41
|
'acceptance-spec.md',
|
|
43
42
|
]),
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Both QA front-ends (`/qa-explore` and `/qa-run`) need to load the
|
|
5
5
|
* *grounded* surface context for an Epic before they reason about what to test:
|
|
6
|
-
* the Epic body
|
|
7
|
-
*
|
|
6
|
+
* the Epic body (which carries the folded Tech Spec sections and
|
|
7
|
+
* Acceptance Table — Story #4324 retired the separate context tickets),
|
|
8
|
+
* the project's `.feature` files, the implementation files the surface
|
|
8
9
|
* map points at, and a slice of recent git history. Today a front-end that
|
|
9
10
|
* trusts in-code comments ("this handler lives at …") can be wrong — the path
|
|
10
11
|
* may have moved, or never existed on the base branch at all. This hydrator
|
|
@@ -37,17 +38,6 @@ export const DEFAULT_BASE_REF = 'main';
|
|
|
37
38
|
/** How many recent commits the hydrator pulls into the context object. */
|
|
38
39
|
export const DEFAULT_LOG_MAX_COUNT = 20;
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
-
* The GitHub label prefixes that mark a linked context ticket. The hydrator
|
|
42
|
-
* resolves `context::prd`, `context::tech-spec`, and `context::acceptance-spec`
|
|
43
|
-
* references off the Epic body and fetches each one through the port.
|
|
44
|
-
*/
|
|
45
|
-
export const CONTEXT_TICKET_KINDS = Object.freeze([
|
|
46
|
-
'prd',
|
|
47
|
-
'tech-spec',
|
|
48
|
-
'acceptance-spec',
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
41
|
/**
|
|
52
42
|
* @typedef {object} GithubPort
|
|
53
43
|
* @property {(issueNumber: number) => Promise<{
|
|
@@ -73,57 +63,6 @@ export const CONTEXT_TICKET_KINDS = Object.freeze([
|
|
|
73
63
|
* @property {string} [note] Free-form provenance note (e.g. a code comment).
|
|
74
64
|
*/
|
|
75
65
|
|
|
76
|
-
/**
|
|
77
|
-
* Parse the linked context-ticket issue numbers out of an Epic body.
|
|
78
|
-
*
|
|
79
|
-
* Two reference shapes are recognized, both written by the planning workflows:
|
|
80
|
-
* - A labelled line: `context::prd #3800` (or `context::tech-spec: #3801`).
|
|
81
|
-
* - A "Planning Artifacts" link: `- PRD: #3800`, `- Tech Spec: #3801`,
|
|
82
|
-
* `- Acceptance Spec: #3802`.
|
|
83
|
-
*
|
|
84
|
-
* Returns a map of kind → issue number for whichever kinds are present. A kind
|
|
85
|
-
* that appears more than once keeps the first occurrence; an unparseable line
|
|
86
|
-
* is skipped rather than thrown.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} epicBody
|
|
89
|
-
* @returns {Record<string, number>}
|
|
90
|
-
*/
|
|
91
|
-
export function parseContextTicketRefs(epicBody) {
|
|
92
|
-
const refs = {};
|
|
93
|
-
const body = typeof epicBody === 'string' ? epicBody : '';
|
|
94
|
-
|
|
95
|
-
// Map the human-readable labels back to canonical kinds.
|
|
96
|
-
const labelToKind = {
|
|
97
|
-
prd: 'prd',
|
|
98
|
-
'tech-spec': 'tech-spec',
|
|
99
|
-
'tech spec': 'tech-spec',
|
|
100
|
-
techspec: 'tech-spec',
|
|
101
|
-
'acceptance-spec': 'acceptance-spec',
|
|
102
|
-
'acceptance spec': 'acceptance-spec',
|
|
103
|
-
acceptancespec: 'acceptance-spec',
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
for (const rawLine of body.split('\n')) {
|
|
107
|
-
const line = rawLine.trim();
|
|
108
|
-
// Match `context::tech-spec #N`, `Tech Spec: #N`, `- PRD: #N`, etc.
|
|
109
|
-
const match = line.match(
|
|
110
|
-
/(?:context::)?([A-Za-z][A-Za-z -]*?)\s*[:#]*\s*#(\d+)/,
|
|
111
|
-
);
|
|
112
|
-
if (!match) continue;
|
|
113
|
-
const key = match[1]
|
|
114
|
-
.trim()
|
|
115
|
-
.toLowerCase()
|
|
116
|
-
.replace(/^context::/, '');
|
|
117
|
-
const kind = labelToKind[key];
|
|
118
|
-
if (!kind) continue;
|
|
119
|
-
const issueNumber = Number(match[2]);
|
|
120
|
-
if (!Number.isInteger(issueNumber)) continue;
|
|
121
|
-
if (refs[kind] === undefined) refs[kind] = issueNumber;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return refs;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
66
|
/**
|
|
128
67
|
* Enumerate the `.feature` files under `featureRoot`, returning repo-relative
|
|
129
68
|
* POSIX-style paths sorted for determinism. A missing root yields an empty
|
|
@@ -189,9 +128,9 @@ export async function verifySurfaceMap(surfaceMap, gitPort, baseRef) {
|
|
|
189
128
|
* Hydrate the QA context object for an Epic.
|
|
190
129
|
*
|
|
191
130
|
* Assembles, in one object:
|
|
192
|
-
* - `epic` — the Epic's `{ number, body, labels }`.
|
|
193
|
-
*
|
|
194
|
-
*
|
|
131
|
+
* - `epic` — the Epic's `{ number, body, labels }`. The body is
|
|
132
|
+
* the single planning document (ideation sections +
|
|
133
|
+
* folded Tech Spec sections + Acceptance Table).
|
|
195
134
|
* - `featureFiles` — repo-relative paths of the project's `.feature` files.
|
|
196
135
|
* - `implementation` — the verified surface map (each entry carries
|
|
197
136
|
* `verified` against the base ref).
|
|
@@ -214,9 +153,6 @@ export async function verifySurfaceMap(surfaceMap, gitPort, baseRef) {
|
|
|
214
153
|
* }} opts
|
|
215
154
|
* @returns {Promise<{
|
|
216
155
|
* epic: { number: number, body: string, labels: string[] },
|
|
217
|
-
* contextTickets: Record<string, {
|
|
218
|
-
* number: number, body: string, labels: string[],
|
|
219
|
-
* }>,
|
|
220
156
|
* featureFiles: string[],
|
|
221
157
|
* implementation: Array<{ path: string, note: string | null, verified: boolean }>,
|
|
222
158
|
* gitLog: Array<{ sha: string, subject: string }>,
|
|
@@ -263,20 +199,6 @@ export async function hydrateQaContext(opts) {
|
|
|
263
199
|
labels: Array.isArray(epicIssue.labels) ? [...epicIssue.labels] : [],
|
|
264
200
|
};
|
|
265
201
|
|
|
266
|
-
// Resolve and fetch the linked context tickets off the Epic body.
|
|
267
|
-
const refs = parseContextTicketRefs(epic.body);
|
|
268
|
-
const contextTickets = {};
|
|
269
|
-
for (const kind of CONTEXT_TICKET_KINDS) {
|
|
270
|
-
const number = refs[kind];
|
|
271
|
-
if (number === undefined) continue;
|
|
272
|
-
const issue = await githubPort.fetchIssue(number);
|
|
273
|
-
contextTickets[kind] = {
|
|
274
|
-
number: issue.number,
|
|
275
|
-
body: issue.body ?? '',
|
|
276
|
-
labels: Array.isArray(issue.labels) ? [...issue.labels] : [],
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
202
|
const featureFiles = collectFeatureFiles(featureRoot, { fsImpl });
|
|
281
203
|
const implementation = await verifySurfaceMap(surfaceMap, gitPort, baseRef);
|
|
282
204
|
const gitLog = await gitPort.recentLog({ maxCount: logMaxCount });
|
|
@@ -286,7 +208,6 @@ export async function hydrateQaContext(opts) {
|
|
|
286
208
|
|
|
287
209
|
return {
|
|
288
210
|
epic,
|
|
289
|
-
contextTickets,
|
|
290
211
|
featureFiles,
|
|
291
212
|
implementation,
|
|
292
213
|
gitLog: Array.isArray(gitLog) ? gitLog : [],
|
|
@@ -52,12 +52,16 @@ function render2TierPrompt({ maxTickets, maxTokenBudget, epicId = null }) {
|
|
|
52
52
|
// (ticket-validator-sizing.js) so the prompt and the validator cannot drift.
|
|
53
53
|
const { softFiles, hardFiles, maxAcceptance, softAcceptanceCount } =
|
|
54
54
|
DEFAULT_TASK_SIZING;
|
|
55
|
-
// Deliverable-granularity definition + single-consumer merge rule
|
|
56
|
-
// sourced from the single
|
|
57
|
-
// (ticket-validator-sizing.js) so
|
|
58
|
-
// cannot drift (Story #3777
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// Deliverable-granularity definition + single-consumer merge rule + the
|
|
56
|
+
// soft envelope-floor heuristic are sourced from the single
|
|
57
|
+
// DELIVERABLE_GRANULARITY_GUIDANCE constant (ticket-validator-sizing.js) so
|
|
58
|
+
// the prompt and the authoring SKILL cannot drift (Story #3777; the
|
|
59
|
+
// envelope-floor sentence added by Story #4313).
|
|
60
|
+
const {
|
|
61
|
+
definition: granularityDefinition,
|
|
62
|
+
singleConsumerRule,
|
|
63
|
+
envelopeFloor,
|
|
64
|
+
} = DELIVERABLE_GRANULARITY_GUIDANCE;
|
|
61
65
|
// The binding-vs-advisory authoring altitude + the New-File Contract are
|
|
62
66
|
// sourced from the single AUTHORING_ALTITUDE_GUIDANCE constant
|
|
63
67
|
// (ticket-validator-sizing.js) so the prompt and the authoring SKILL cannot
|
|
@@ -75,7 +79,7 @@ function render2TierPrompt({ maxTickets, maxTokenBudget, epicId = null }) {
|
|
|
75
79
|
? `@epic-${epicId}-ac-1`
|
|
76
80
|
: '@epic-<id>-ac-N';
|
|
77
81
|
return `You are an expert Senior Project Manager and Orchestrator.
|
|
78
|
-
Your job is to take
|
|
82
|
+
Your job is to take an Epic (including its inline User Stories) and a Technical Specification and decompose them into a flat list of Story tickets for an AI Agent to execute.
|
|
79
83
|
|
|
80
84
|
### HIERARCHY RULES:
|
|
81
85
|
1. **Stories**: Specific user-facing or architectural user stories (e.g., "Implement JWT Token Exchange").
|
|
@@ -162,6 +166,8 @@ The primary question is **cohesion, not count**: *is this one coherent change wi
|
|
|
162
166
|
|
|
163
167
|
**Size against the real one-pass delivery envelope.** Each Story is delivered and self-verified by a single agent in one pass, whose context is capped by the delivery token budget \`maxTokenBudget = ${maxTokenBudget}\` tokens (the task-prompt hydration cap). Use that envelope — not the file count alone — as the leading sizing input: a Story is correctly sized when one agent can hold its full change, acceptance, and verification in a single pass within \`maxTokenBudget\`. The numeric file thresholds below are a coarse backstop on top of this envelope, not the primary signal.
|
|
164
168
|
|
|
169
|
+
${envelopeFloor}
|
|
170
|
+
|
|
165
171
|
- **One Story = one coherent change with one reason to exist.** If you cannot state that reason in a sentence, the Story is probably two Stories — or two Stories that should be one. State that sentence explicitly in the Story's \`reason_to_exist\` meta field (see STORY BODY RULES) so the consolidate critic can check it.
|
|
166
172
|
- ${singleConsumerRule}
|
|
167
173
|
- **Split independent, parallelizable work** into sibling Stories — but only when the pieces genuinely have separate reasons to exist.
|
|
@@ -194,7 +200,7 @@ Declaring \`wide\` with a non-empty reason **lifts the \`hardFiles\` rejection**
|
|
|
194
200
|
|
|
195
201
|
#### BRAND / COPY / STYLE WORK:
|
|
196
202
|
|
|
197
|
-
- Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in
|
|
203
|
+
- Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in the Epic body"]\`. Silence on style sourcing is a smell.
|
|
198
204
|
|
|
199
205
|
### WAVE-0 BDD SCAFFOLD STORY (features-first; emit when the Acceptance Spec has \`new\`-disposition rows):
|
|
200
206
|
The Acceptance Spec's AC table (columns \`AC ID | Outcome | Feature File | Scenario | Disposition\`) tags each row's \`Disposition\` with one of \`new | updated | unchanged\`. A \`new\` row names a \`.feature\` file + scenario that does NOT yet exist on \`main\`. The framework is features-first: implementing Stories reference those \`.feature\` paths in their \`verify[]\` lines, so the files MUST already exist when those Stories run — otherwise verification fails mid-delivery on a missing file. (These Gherkin \`.feature\` files are BDD artifacts, unrelated to any ticket tier.)
|
|
@@ -412,7 +412,7 @@ export async function runLifecycleEmit({
|
|
|
412
412
|
// record `failed` classifications instead of throwing (the bus's
|
|
413
413
|
// `onFailed` boundary already persists the originating event), so the
|
|
414
414
|
// bus emit resolves cleanly even when a downstream side effect — e.g.
|
|
415
|
-
// an acceptance-reconcile gap or
|
|
415
|
+
// an acceptance-reconcile gap or an `openOrLocatePr` throw inside
|
|
416
416
|
// the Finalizer — failed. Without surfacing these, the CLI would exit 0
|
|
417
417
|
// with success-shaped JSON despite a partial finalize (Story #3904).
|
|
418
418
|
const outcomes = collectOutcomes(chain);
|
|
@@ -66,7 +66,6 @@ export const KNOWN_AXES = Object.freeze([
|
|
|
66
66
|
'type',
|
|
67
67
|
'status',
|
|
68
68
|
'persona',
|
|
69
|
-
'context',
|
|
70
69
|
'acceptance',
|
|
71
70
|
'meta',
|
|
72
71
|
'planning',
|
|
@@ -156,11 +155,11 @@ export function findVocabularyViolations(src, axes = KNOWN_AXES) {
|
|
|
156
155
|
// - value is a lowercase identifier (`[a-z][a-z0-9-]*`)
|
|
157
156
|
//
|
|
158
157
|
// The whole-body anchor avoids false positives on:
|
|
159
|
-
// - `
|
|
158
|
+
// - `acceptance::n-a` — canonical label, value contains a
|
|
160
159
|
// hyphen but is not at axis position.
|
|
161
160
|
// - `<type>/<slug>` — template placeholders.
|
|
162
161
|
// - `delivery.maxTokenBudget` — config-key paths with `.`.
|
|
163
|
-
// - `
|
|
162
|
+
// - `delivery.{maxTickets}` — JS destructure shapes.
|
|
164
163
|
// - `agent-protocol.md` — filenames / concept slugs.
|
|
165
164
|
//
|
|
166
165
|
// Real drift like `type/epic` matches because the WHOLE body is
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* Extracted from `../github.js` in Story #1846 / Task #1859.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { parseLinkedIssues } from '../../lib/issue-link-parser.js';
|
|
15
|
-
|
|
16
14
|
function normalizeLabels(issue) {
|
|
17
15
|
const raw = issue?.labels;
|
|
18
16
|
if (!raw) return [];
|
|
@@ -50,7 +48,6 @@ export function issueToEpic(issue) {
|
|
|
50
48
|
body: issue.body ?? '',
|
|
51
49
|
labels,
|
|
52
50
|
labelSet: new Set(labels),
|
|
53
|
-
linkedIssues: parseLinkedIssues(issue.body),
|
|
54
51
|
};
|
|
55
52
|
}
|
|
56
53
|
|
|
@@ -112,7 +112,7 @@ export class TicketGateway {
|
|
|
112
112
|
this._cache = cache ?? createInlineTicketCache();
|
|
113
113
|
/**
|
|
114
114
|
* Per-instance memo of `getTickets(epicId, filters)` results (Story
|
|
115
|
-
* #3988).
|
|
115
|
+
* #3988). Planning-era healing fetched the same child list twice
|
|
116
116
|
* per planning pass; without this memo each fetch re-pays the full
|
|
117
117
|
* search/list round-trip. Invalidated on every write surface.
|
|
118
118
|
* @type {Map<string, object[]>}
|
|
@@ -322,24 +322,13 @@ export class TicketGateway {
|
|
|
322
322
|
// Mirror the Epic create path (issues.js:160 → `labels: TYPE_LABELS.EPIC`):
|
|
323
323
|
// inject TYPE_LABELS.STORY so a spec that omits the labels array cannot
|
|
324
324
|
// produce an unlabeled, undispatchable Story. Dedupe to avoid duplicates
|
|
325
|
-
// when the caller already carries the label.
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// ticket class created through this same factory carrying a `context::*`
|
|
329
|
-
// label (and no `type::story`). They MUST NOT be stamped `type::story`:
|
|
330
|
-
// doing so makes every `type::story`-counting consumer — the decompose
|
|
331
|
-
// open-children guard (`assertNoOpenPlanChildren`), the delivery wave
|
|
332
|
-
// builder (`discoverOpenStories`), the plan healthcheck — mis-classify
|
|
333
|
-
// them as deliverable Stories. Skip the injection when the caller's labels
|
|
334
|
-
// already classify the ticket as context.
|
|
325
|
+
// when the caller already carries the label. (Story #4324 retired the
|
|
326
|
+
// `context::*` ticket classes, so every ticket created through this
|
|
327
|
+
// factory is a Story.)
|
|
335
328
|
const callerLabels = ticketData.labels ?? [];
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const labels =
|
|
340
|
-
isContextTicket || callerLabels.includes(TYPE_LABELS.STORY)
|
|
341
|
-
? callerLabels
|
|
342
|
-
: [TYPE_LABELS.STORY, ...callerLabels];
|
|
329
|
+
const labels = callerLabels.includes(TYPE_LABELS.STORY)
|
|
330
|
+
? callerLabels
|
|
331
|
+
: [TYPE_LABELS.STORY, ...callerLabels];
|
|
343
332
|
const result = await this._gh.api({
|
|
344
333
|
method: 'POST',
|
|
345
334
|
endpoint: `/repos/${this.owner}/${this.repo}/issues`,
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
* 6. Flip the Story to `agent::executing`.
|
|
24
24
|
*
|
|
25
25
|
* What this script does NOT do (and why):
|
|
26
|
-
* - Skips `traceHierarchy` — no Epic → no PRD/Tech-Spec.
|
|
27
26
|
* - Skips `validateBlockers` against the body's `Blocked by:` markers —
|
|
28
27
|
* pre-flight is still the operator's responsibility, but the Epic-scope
|
|
29
28
|
* blocker chain doesn't fit.
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* pipeline stages from `lib/story-init/`:
|
|
10
10
|
*
|
|
11
11
|
* 1. context-resolver — fetch the Story + optionally mark as recut.
|
|
12
|
-
* 2. hierarchy-tracer — resolve Feature/Epic → PRD / Tech Spec.
|
|
13
12
|
* 3. blocker-validator — refuse to proceed while dependencies are open.
|
|
14
13
|
* 4. task-graph-builder — fetch + topologically sort child Tasks.
|
|
15
14
|
* 5. branch-initializer — materialise the story branch (single-tree
|
|
@@ -53,7 +52,6 @@ import {
|
|
|
53
52
|
planStoryBranchSeed,
|
|
54
53
|
} from './lib/story-init/branch-initializer.js';
|
|
55
54
|
import { resolveContext } from './lib/story-init/context-resolver.js';
|
|
56
|
-
import { traceHierarchy } from './lib/story-init/hierarchy-tracer.js';
|
|
57
55
|
import { transitionStoryToExecuting } from './lib/story-init/state-transitioner.js';
|
|
58
56
|
import { buildTaskGraph } from './lib/story-init/task-graph-builder.js';
|
|
59
57
|
import { createPhaseTimer } from './lib/util/phase-timer.js';
|
|
@@ -88,8 +86,6 @@ export async function runStoryInit({
|
|
|
88
86
|
dryRun: dryRunParam,
|
|
89
87
|
cwd: cwdParam,
|
|
90
88
|
recutOf: recutOfParam,
|
|
91
|
-
prdId: prdIdParam,
|
|
92
|
-
techSpecId: techSpecIdParam,
|
|
93
89
|
injectedProvider,
|
|
94
90
|
injectedConfig,
|
|
95
91
|
} = {}) {
|
|
@@ -100,17 +96,10 @@ export async function runStoryInit({
|
|
|
100
96
|
dryRun: !!dryRunParam,
|
|
101
97
|
cwd: cwdParam ?? null,
|
|
102
98
|
recutOf: recutOfParam ?? null,
|
|
103
|
-
prdId: prdIdParam ?? null,
|
|
104
|
-
techSpecId: techSpecIdParam ?? null,
|
|
105
99
|
}
|
|
106
100
|
: parseSprintArgs();
|
|
107
101
|
const { storyId, dryRun } = parsed;
|
|
108
102
|
const recutOf = recutOfParam ?? parsed.recutOf ?? null;
|
|
109
|
-
// Story #4253: pre-resolved Epic linkages (from the /deliver fan-out's
|
|
110
|
-
// one-time Epic resolution). When both are present, hierarchy-tracer skips
|
|
111
|
-
// the per-Story getEpic round-trip; when absent it resolves them itself.
|
|
112
|
-
const threadedPrdId = prdIdParam ?? parsed.prdId ?? null;
|
|
113
|
-
const threadedTechSpecId = techSpecIdParam ?? parsed.techSpecId ?? null;
|
|
114
103
|
// Worktree-aware cwd resolution: explicit param > --cwd flag > env > PROJECT_ROOT.
|
|
115
104
|
const cwd = path.resolve(cwdParam ?? parsed.cwd ?? PROJECT_ROOT);
|
|
116
105
|
|
|
@@ -155,20 +144,7 @@ export async function runStoryInit({
|
|
|
155
144
|
input: { storyId, recutOf, dryRun },
|
|
156
145
|
});
|
|
157
146
|
|
|
158
|
-
// Stage 2 — hierarchy. When the /deliver fan-out threaded --prd/--tech-spec
|
|
159
|
-
// (both resolved once by the parent), this short-circuits the per-Story
|
|
160
|
-
// getEpic. Absent flags fall back to the legacy getEpic resolution.
|
|
161
|
-
const { prdId, techSpecId } = await traceHierarchy({
|
|
162
|
-
provider,
|
|
163
|
-
logger: stageLogger,
|
|
164
|
-
input: { epicId, prdId: threadedPrdId, techSpecId: threadedTechSpecId },
|
|
165
|
-
});
|
|
166
|
-
|
|
167
147
|
progress('CONTEXT', `Epic: #${epicId}, Parent: #${parentId ?? 'none'}`);
|
|
168
|
-
progress(
|
|
169
|
-
'CONTEXT',
|
|
170
|
-
`PRD: #${prdId ?? 'none'}, Tech Spec: #${techSpecId ?? 'none'}`,
|
|
171
|
-
);
|
|
172
148
|
|
|
173
149
|
// Stage 3 — blockers.
|
|
174
150
|
const { openBlockers } = await validateBlockers({
|
|
@@ -335,8 +311,6 @@ export async function runStoryInit({
|
|
|
335
311
|
installStatus,
|
|
336
312
|
sortedTasks,
|
|
337
313
|
parentId,
|
|
338
|
-
prdId,
|
|
339
|
-
techSpecId,
|
|
340
314
|
dryRun,
|
|
341
315
|
recutOf,
|
|
342
316
|
hierarchy: hierarchyMode,
|
|
@@ -473,8 +447,6 @@ function buildStoryInitResult({
|
|
|
473
447
|
installStatus,
|
|
474
448
|
sortedTasks,
|
|
475
449
|
parentId,
|
|
476
|
-
prdId,
|
|
477
|
-
techSpecId,
|
|
478
450
|
dryRun,
|
|
479
451
|
recutOf,
|
|
480
452
|
hierarchy,
|
|
@@ -506,7 +478,7 @@ function buildStoryInitResult({
|
|
|
506
478
|
labels: t.labels,
|
|
507
479
|
dependencies: t.dependsOn ?? parseBlockedBy(t.body ?? ''),
|
|
508
480
|
})),
|
|
509
|
-
context: { parentId
|
|
481
|
+
context: { parentId },
|
|
510
482
|
dryRun,
|
|
511
483
|
};
|
|
512
484
|
}
|