@yaawn/workflow 0.0.0-stage → 0.3.1
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/README.md +172 -2
- package/agents/instructions/output-styles/yaawn-eli5.instructions.md +45 -0
- package/agents/instructions/yaawn-accessibility.css.instructions.md +58 -0
- package/agents/instructions/yaawn-accessibility.html.instructions.md +90 -0
- package/agents/instructions/yaawn-accessibility.javascript.instructions.md +50 -0
- package/agents/instructions/yaawn-clean-code.typescript.instructions.md +74 -0
- package/agents/instructions/yaawn-function-coupling.typescript.instructions.md +100 -0
- package/agents/instructions/yaawn-html-css-js.html.instructions.md +32 -0
- package/agents/instructions/yaawn-open-source-first.instructions.md +38 -0
- package/agents/instructions/yaawn-personas.instructions.md +18 -0
- package/agents/instructions/yaawn-semantic-html.html.instructions.md +40 -0
- package/agents/instructions/yaawn-vanilla-css.css.instructions.md +50 -0
- package/agents/output-styles/yaawn-eli5.instructions.md +45 -0
- package/agents/personas/yaawn-developer.md +16 -0
- package/agents/personas/yaawn-product-owner.md +18 -0
- package/agents/personas/yaawn-reviewer.md +16 -0
- package/agents/skills/yaawn-clean-code/SKILL.md +61 -0
- package/agents/skills/yaawn-clean-code/references/classes.md +114 -0
- package/agents/skills/yaawn-clean-code/references/comments.md +164 -0
- package/agents/skills/yaawn-clean-code/references/error-handling.md +151 -0
- package/agents/skills/yaawn-clean-code/references/formatting.md +146 -0
- package/agents/skills/yaawn-clean-code/references/functions.md +261 -0
- package/agents/skills/yaawn-clean-code/references/meaningful-names.md +234 -0
- package/agents/skills/yaawn-clean-code/references/objects-and-data-structures.md +130 -0
- package/agents/skills/yaawn-clean-code/references/principles.md +76 -0
- package/agents/skills/yaawn-clean-code/references/simple-design-and-code-smells.md +130 -0
- package/agents/skills/yaawn-clean-code/references/unit-tests.md +114 -0
- package/agents/skills/yaawn-create-plan/SKILL.md +228 -0
- package/agents/skills/yaawn-grill-bug-report/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-bug-report/references/bug-report-checklist.md +27 -0
- package/agents/skills/yaawn-grill-design/SKILL.md +68 -0
- package/agents/skills/yaawn-grill-design/references/design-checklist.md +36 -0
- package/agents/skills/yaawn-grill-other/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-other/references/other-checklist.md +19 -0
- package/agents/skills/yaawn-grill-refactor/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-refactor/references/refactor-checklist.md +26 -0
- package/agents/skills/yaawn-grill-spike/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-spike/references/spike-checklist.md +25 -0
- package/agents/skills/yaawn-grill-story/SKILL.md +34 -0
- package/agents/skills/yaawn-grill-story/references/concept-challenge.md +67 -0
- package/agents/skills/yaawn-grill-story/references/story-checklist.md +43 -0
- package/agents/skills/yaawn-implement-plan/SKILL.md +196 -0
- package/agents/skills/yaawn-implement-plan/assets/fix-prompt.md +66 -0
- package/agents/skills/yaawn-implement-plan/assets/review-prompt.md +89 -0
- package/agents/skills/yaawn-implement-plan/assets/session-prompt.md +80 -0
- package/agents/skills/yaawn-implement-plan/scripts/agent.ts +218 -0
- package/agents/skills/yaawn-implement-plan/scripts/caffeinate.ts +56 -0
- package/agents/skills/yaawn-implement-plan/scripts/groups.ts +233 -0
- package/agents/skills/yaawn-implement-plan/scripts/overnight.ts +772 -0
- package/agents/skills/yaawn-implement-plan/scripts/parallel.ts +1176 -0
- package/agents/skills/yaawn-implement-plan/scripts/processes.ts +53 -0
- package/agents/skills/yaawn-implement-plan/scripts/stages.ts +65 -0
- package/agents/skills/yaawn-implement-plan/scripts/targets.ts +381 -0
- package/agents/skills/yaawn-implement-plan/scripts/worktrees.ts +431 -0
- package/agents/skills/yaawn-owasp-security/SKILL.md +106 -0
- package/agents/skills/yaawn-owasp-security/references/a01-broken-access-control.md +58 -0
- package/agents/skills/yaawn-owasp-security/references/a02-security-misconfiguration.md +56 -0
- package/agents/skills/yaawn-owasp-security/references/a03-software-supply-chain-failures.md +56 -0
- package/agents/skills/yaawn-owasp-security/references/a04-cryptographic-failures.md +67 -0
- package/agents/skills/yaawn-owasp-security/references/a05-injection.md +53 -0
- package/agents/skills/yaawn-owasp-security/references/a06-insecure-design.md +48 -0
- package/agents/skills/yaawn-owasp-security/references/a07-authentication-failures.md +68 -0
- package/agents/skills/yaawn-owasp-security/references/a08-software-data-integrity-failures.md +45 -0
- package/agents/skills/yaawn-owasp-security/references/a09-security-logging-alerting-failures.md +59 -0
- package/agents/skills/yaawn-owasp-security/references/a10-mishandling-exceptional-conditions.md +63 -0
- package/agents/skills/yaawn-prepare-plan/SKILL.md +110 -0
- package/agents/skills/yaawn-save-plan/SKILL.md +65 -0
- package/agents/skills/yaawn-save-plan/scripts/plan-body.ts +331 -0
- package/agents/skills/yaawn-save-plan/scripts/save-plan.ts +443 -0
- package/agents/skills/yaawn-split-story/SKILL.md +189 -0
- package/agents/skills/yaawn-split-story/references/spider.md +64 -0
- package/agents/skills/yaawn-split-story/scripts/create-issue.ts +162 -0
- package/agents/skills/yaawn-verify-executed-plan/SKILL.md +154 -0
- package/agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts +362 -0
- package/agents/skills/yaawn-verify-executed-plan/scripts/pull-request.ts +131 -0
- package/agents/skills/yaawn-write-bug-report/SKILL.md +87 -0
- package/agents/skills/yaawn-write-other/SKILL.md +90 -0
- package/agents/skills/yaawn-write-refactor/SKILL.md +94 -0
- package/agents/skills/yaawn-write-spike/SKILL.md +94 -0
- package/agents/skills/yaawn-write-user-story/SKILL.md +187 -0
- package/agents/skills/yaawn-write-user-story/references/acceptance-criteria.md +186 -0
- package/agents/skills/yaawn-write-user-story/references/five-types-of-value.md +82 -0
- package/agents/skills/yaawn-write-user-story/references/user-story-fundamentals.md +75 -0
- package/agents/yaawn-glossary.md +146 -0
- package/agents/yaawn-grilling.md +75 -0
- package/agents/yaawn-worktrees.md +69 -0
- package/config/install-tasks.json +35 -0
- package/config/settings.json +23 -0
- package/config/wizard.json +128 -0
- package/github/ISSUE_TEMPLATE/yaawn-bug-report.md +59 -0
- package/github/ISSUE_TEMPLATE/yaawn-other.md +38 -0
- package/github/ISSUE_TEMPLATE/yaawn-refactor.md +38 -0
- package/github/ISSUE_TEMPLATE/yaawn-spike.md +16 -0
- package/github/ISSUE_TEMPLATE/yaawn-story.md +56 -0
- package/package.json +33 -4
- package/scripts/install.js +73 -0
- package/yaawn/gitignore +1 -0
- package/yaawn/yaawn-definition-of-done.md +5 -0
- package/yaawn/yaawn-project-glossary.md +7 -0
- package/yaawn/yaawn-user-roles.md +3 -0
- package/yaawn/yaawn-verification-commands.md +7 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
dependsOnLine,
|
|
13
|
+
removeGroupLabels,
|
|
14
|
+
} from "../../yaawn-implement-plan/scripts/targets.js";
|
|
15
|
+
import {
|
|
16
|
+
branchName,
|
|
17
|
+
branchWorktreeFromPorcelain,
|
|
18
|
+
currentToplevel,
|
|
19
|
+
defaultBranches,
|
|
20
|
+
entriesFromPorcelain,
|
|
21
|
+
listPorcelain,
|
|
22
|
+
mainCopyFromPorcelain,
|
|
23
|
+
} from "../../yaawn-implement-plan/scripts/worktrees.js";
|
|
24
|
+
import {
|
|
25
|
+
type Section,
|
|
26
|
+
buildBody,
|
|
27
|
+
filesToChangeContent,
|
|
28
|
+
findSection,
|
|
29
|
+
headingList,
|
|
30
|
+
humanReadableTitle,
|
|
31
|
+
ownLlmPlanContent,
|
|
32
|
+
planSubsections,
|
|
33
|
+
readyBlocker,
|
|
34
|
+
toLines,
|
|
35
|
+
} from "./plan-body.js";
|
|
36
|
+
|
|
37
|
+
const plansDir = ".yaawn/plans";
|
|
38
|
+
const refinementLabel = "Yaawn: To Be Refined";
|
|
39
|
+
const readyLabel = "Yaawn: Ready To Implement";
|
|
40
|
+
const readyLabelColor = "0E8A16";
|
|
41
|
+
const readyLabelDescription =
|
|
42
|
+
"Plan saved, no open questions — ready for /yaawn-implement-plan";
|
|
43
|
+
const usage =
|
|
44
|
+
"Usage: npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts <find [issue]|preview <issue>|save <issue>>";
|
|
45
|
+
|
|
46
|
+
const issuePattern = /^[1-9]\d*$/;
|
|
47
|
+
const planFilePattern = /^yaawn-([1-9]\d*)-plan\.md$/;
|
|
48
|
+
const issueBranchPattern = /^yaawn\/[1-9]\d*$/;
|
|
49
|
+
|
|
50
|
+
export type Resolution<T> = { ok: true; value: T } | { ok: false; reason: string };
|
|
51
|
+
|
|
52
|
+
export interface WorktreeSearch {
|
|
53
|
+
porcelain: string;
|
|
54
|
+
currentToplevel: string;
|
|
55
|
+
defaultBranches: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PlanFiles {
|
|
59
|
+
plan: string;
|
|
60
|
+
humanReadable: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PlanCandidate {
|
|
64
|
+
issue: number;
|
|
65
|
+
worktree: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const planPath = (worktree: string, issue: number): string =>
|
|
69
|
+
path.join(worktree, plansDir, `yaawn-${issue}-plan.md`);
|
|
70
|
+
|
|
71
|
+
const humanReadablePath = (worktree: string, issue: number): string =>
|
|
72
|
+
path.join(worktree, plansDir, `yaawn-${issue}-human-readable.md`);
|
|
73
|
+
|
|
74
|
+
// The issue body snapshots always stay in the current worktree.
|
|
75
|
+
const currentBodyPath = (issue: number): string =>
|
|
76
|
+
`${plansDir}/yaawn-${issue}-body-current.md`;
|
|
77
|
+
|
|
78
|
+
const newBodyPath = (issue: number): string =>
|
|
79
|
+
`${plansDir}/yaawn-${issue}-body.md`;
|
|
80
|
+
|
|
81
|
+
// The issue number ends up in file paths and branch names, so only a
|
|
82
|
+
// positive integer is accepted.
|
|
83
|
+
export function parseIssue(argument: string | undefined): number | null {
|
|
84
|
+
if (!issuePattern.test(argument ?? "")) return null;
|
|
85
|
+
const issue = Number(argument);
|
|
86
|
+
return Number.isSafeInteger(issue) ? issue : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const uniquePaths = (worktrees: (string | null)[]): string[] => [
|
|
90
|
+
...new Set(
|
|
91
|
+
worktrees
|
|
92
|
+
.filter((worktree): worktree is string => worktree !== null)
|
|
93
|
+
.map((worktree) => path.resolve(worktree)),
|
|
94
|
+
),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// --- plan file resolution
|
|
98
|
+
|
|
99
|
+
// Order matters: the current worktree first, then the issue's own, then the
|
|
100
|
+
// main copy.
|
|
101
|
+
export function planCandidates(issue: number, search: WorktreeSearch): string[] {
|
|
102
|
+
return uniquePaths([
|
|
103
|
+
search.currentToplevel,
|
|
104
|
+
branchWorktreeFromPorcelain(search.porcelain, branchName(issue)),
|
|
105
|
+
mainCopyFromPorcelain(search.porcelain, search.defaultBranches),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const listedWorktrees = (worktrees: string[]): string =>
|
|
110
|
+
worktrees.map((worktree) => ` - ${worktree}`).join("\n");
|
|
111
|
+
|
|
112
|
+
// The current worktree wins; elsewhere exactly one holder is required — a
|
|
113
|
+
// guess between several could save the wrong plan.
|
|
114
|
+
export function resolvePlanDir(
|
|
115
|
+
issue: number,
|
|
116
|
+
candidates: string[],
|
|
117
|
+
hasFile: (file: string) => boolean,
|
|
118
|
+
): Resolution<string> {
|
|
119
|
+
const [current, ...others] = candidates;
|
|
120
|
+
if (current !== undefined && hasFile(planPath(current, issue))) {
|
|
121
|
+
return { ok: true, value: current };
|
|
122
|
+
}
|
|
123
|
+
const holders = others.filter((worktree) => hasFile(planPath(worktree, issue)));
|
|
124
|
+
if (holders.length === 1) return { ok: true, value: holders[0] as string };
|
|
125
|
+
if (holders.length > 1) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: `several worktrees hold yaawn-${issue}-plan.md — run this skill in the one whose plan you want to save:\n${listedWorktrees(holders)}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: `no worktree holds yaawn-${issue}-plan.md — start a new session and run /yaawn-create-plan ${issue} first; searched:\n${listedWorktrees(candidates)}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- issue inference
|
|
138
|
+
|
|
139
|
+
export function inferenceCandidates(search: WorktreeSearch): string[] {
|
|
140
|
+
const issueWorktrees = entriesFromPorcelain(search.porcelain)
|
|
141
|
+
.filter((entry) => !entry.bare && issueBranchPattern.test(entry.branch ?? ""))
|
|
142
|
+
.map((entry) => entry.path);
|
|
143
|
+
return uniquePaths([
|
|
144
|
+
search.currentToplevel,
|
|
145
|
+
...issueWorktrees,
|
|
146
|
+
mainCopyFromPorcelain(search.porcelain, search.defaultBranches),
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const plansIn = (
|
|
151
|
+
worktree: string,
|
|
152
|
+
planFiles: (worktree: string) => string[],
|
|
153
|
+
): PlanCandidate[] =>
|
|
154
|
+
planFiles(worktree)
|
|
155
|
+
.map((file) => planFilePattern.exec(file)?.[1])
|
|
156
|
+
.filter((issue): issue is string => issue !== undefined)
|
|
157
|
+
.map((issue) => ({ issue: Number(issue), worktree }));
|
|
158
|
+
|
|
159
|
+
const listedPlans = (plans: PlanCandidate[]): string =>
|
|
160
|
+
plans.map(({ issue, worktree }) => ` - #${issue} in ${worktree}`).join("\n");
|
|
161
|
+
|
|
162
|
+
export function inferIssue(
|
|
163
|
+
candidates: string[],
|
|
164
|
+
planFiles: (worktree: string) => string[],
|
|
165
|
+
): Resolution<number> {
|
|
166
|
+
const plans = candidates.flatMap((worktree) => plansIn(worktree, planFiles));
|
|
167
|
+
const issues = new Set(plans.map(({ issue }) => issue));
|
|
168
|
+
if (issues.size === 1) return { ok: true, value: [...issues][0] as number };
|
|
169
|
+
if (issues.size > 1) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: `several plan files found — which issue should be saved?\n${listedPlans(plans)}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
reason: "no plan file found — which issue number should be saved?",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- git and file system
|
|
182
|
+
|
|
183
|
+
// Without git worktrees (no git, no repository) only the current directory
|
|
184
|
+
// is searched — the behavior before worktree support.
|
|
185
|
+
function worktreeSearch(): WorktreeSearch | null {
|
|
186
|
+
try {
|
|
187
|
+
return {
|
|
188
|
+
porcelain: listPorcelain(),
|
|
189
|
+
currentToplevel: currentToplevel(),
|
|
190
|
+
defaultBranches: defaultBranches(),
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function planFilesOf(worktree: string): string[] {
|
|
198
|
+
try {
|
|
199
|
+
return readdirSync(path.join(worktree, plansDir));
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const unwrap = <T>(resolution: Resolution<T>): T => {
|
|
206
|
+
if (!resolution.ok) throw new Error(resolution.reason);
|
|
207
|
+
return resolution.value;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
function findPlanFiles(issue: number, search: WorktreeSearch | null): PlanFiles {
|
|
211
|
+
const candidates = search ? planCandidates(issue, search) : [process.cwd()];
|
|
212
|
+
const worktree = unwrap(resolvePlanDir(issue, candidates, existsSync));
|
|
213
|
+
return {
|
|
214
|
+
plan: planPath(worktree, issue),
|
|
215
|
+
humanReadable: humanReadablePath(worktree, issue),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function findIssue(search: WorktreeSearch | null): number {
|
|
220
|
+
const candidates = search ? inferenceCandidates(search) : [process.cwd()];
|
|
221
|
+
return unwrap(inferIssue(candidates, planFilesOf));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function gh(args: string[]): string {
|
|
225
|
+
return execFileSync("gh", args, { encoding: "utf-8" });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readRequiredFile(file: string, hint: string): string {
|
|
229
|
+
if (!existsSync(file)) throw new Error(`${file} not found — ${hint}`);
|
|
230
|
+
const content = readFileSync(file, "utf-8").trim();
|
|
231
|
+
if (!content) throw new Error(`${file} is empty — ${hint}`);
|
|
232
|
+
return content;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readPlanSubsections(planFile: string): Map<string, string> {
|
|
236
|
+
const lines = toLines(readRequiredFile(planFile, "run /yaawn-create-plan first"));
|
|
237
|
+
const sections = planSubsections.map((title) => ({
|
|
238
|
+
title,
|
|
239
|
+
section: findSection(lines, 3, title),
|
|
240
|
+
}));
|
|
241
|
+
const missing = sections.filter(({ section }) => section === null);
|
|
242
|
+
if (missing.length > 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`${planFile} lacks: ${headingList(missing.map(({ title }) => title))}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return new Map(
|
|
248
|
+
sections.map(({ title, section }) => [
|
|
249
|
+
title,
|
|
250
|
+
lines
|
|
251
|
+
.slice((section as Section).contentStart, (section as Section).contentEnd)
|
|
252
|
+
.join("\n")
|
|
253
|
+
.trim(),
|
|
254
|
+
]),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function readNewContent(files: PlanFiles): Map<string, string> {
|
|
259
|
+
const content = readPlanSubsections(files.plan);
|
|
260
|
+
content.set(
|
|
261
|
+
humanReadableTitle,
|
|
262
|
+
readRequiredFile(
|
|
263
|
+
files.humanReadable,
|
|
264
|
+
"write the Human Readable summary first",
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
return content;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function fetchBody(issue: number): string {
|
|
271
|
+
return gh(["issue", "view", String(issue), "--json", "body", "--jq", ".body"])
|
|
272
|
+
.replace(/\r\n/g, "\n")
|
|
273
|
+
.trimEnd();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// diff exits 1 when the files differ; its output is the result, not an error.
|
|
277
|
+
function diffOf(currentPath: string, newPath: string): string {
|
|
278
|
+
try {
|
|
279
|
+
execFileSync("diff", ["-u", currentPath, newPath], { encoding: "utf-8" });
|
|
280
|
+
return "no changes";
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const { stdout } = error as { stdout?: string };
|
|
283
|
+
if (stdout) return stdout;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function preview(issue: number, files: PlanFiles): void {
|
|
289
|
+
const content = readNewContent(files);
|
|
290
|
+
const body = fetchBody(issue);
|
|
291
|
+
writeFileSync(currentBodyPath(issue), `${body}\n`);
|
|
292
|
+
writeFileSync(newBodyPath(issue), buildBody(body, content));
|
|
293
|
+
console.log(`existing plan: ${ownLlmPlanContent(body) ? "yes" : "no"}`);
|
|
294
|
+
console.log("--- diff of the issue body");
|
|
295
|
+
console.log(diffOf(currentBodyPath(issue), newBodyPath(issue)));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function issueLabels(issue: number): string[] {
|
|
299
|
+
return gh([
|
|
300
|
+
"issue",
|
|
301
|
+
"view",
|
|
302
|
+
String(issue),
|
|
303
|
+
"--json",
|
|
304
|
+
"labels",
|
|
305
|
+
"--jq",
|
|
306
|
+
".labels[].name",
|
|
307
|
+
])
|
|
308
|
+
.split("\n")
|
|
309
|
+
.filter((label) => label !== "");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hasLabel(issue: number, label: string): boolean {
|
|
313
|
+
return issueLabels(issue).includes(label);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function removeRefinementLabel(issue: number): void {
|
|
317
|
+
if (!hasLabel(issue, refinementLabel)) {
|
|
318
|
+
console.log(`label "${refinementLabel}" not present`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
gh(["issue", "edit", String(issue), "--remove-label", refinementLabel]);
|
|
322
|
+
console.log(`removed label "${refinementLabel}"`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const errorMessage = (error: unknown): string =>
|
|
326
|
+
error instanceof Error ? error.message.trim() : String(error);
|
|
327
|
+
|
|
328
|
+
// Runs after the body is saved: a label failure must not fail the save.
|
|
329
|
+
function markReady(issue: number, content: Map<string, string>): void {
|
|
330
|
+
const blocker = readyBlocker(content);
|
|
331
|
+
if (blocker) {
|
|
332
|
+
console.log(`not marked ready: ${blocker}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
ensureReadyLabelExists();
|
|
337
|
+
gh(["issue", "edit", String(issue), "--add-label", readyLabel]);
|
|
338
|
+
console.log(`added label "${readyLabel}"`);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.warn(
|
|
341
|
+
`warning: could not add label "${readyLabel}" — add it manually: ${errorMessage(error)}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Both sections feed the implementation grouping, so a change to either
|
|
347
|
+
// invalidates the issue's group labels.
|
|
348
|
+
function groupingInputsChanged(oldBody: string, newBody: string): boolean {
|
|
349
|
+
return (
|
|
350
|
+
filesToChangeContent(oldBody) !== filesToChangeContent(newBody) ||
|
|
351
|
+
dependsOnLine(oldBody) !== dependsOnLine(newBody)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function labelExists(label: string): boolean {
|
|
356
|
+
return gh([
|
|
357
|
+
"label",
|
|
358
|
+
"list",
|
|
359
|
+
"--search",
|
|
360
|
+
label,
|
|
361
|
+
"--limit",
|
|
362
|
+
"100",
|
|
363
|
+
"--json",
|
|
364
|
+
"name",
|
|
365
|
+
"--jq",
|
|
366
|
+
".[].name",
|
|
367
|
+
])
|
|
368
|
+
.split("\n")
|
|
369
|
+
.includes(label);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function ensureReadyLabelExists(): void {
|
|
373
|
+
if (labelExists(readyLabel)) return;
|
|
374
|
+
try {
|
|
375
|
+
gh([
|
|
376
|
+
"label",
|
|
377
|
+
"create",
|
|
378
|
+
readyLabel,
|
|
379
|
+
"--color",
|
|
380
|
+
readyLabelColor,
|
|
381
|
+
"--description",
|
|
382
|
+
readyLabelDescription,
|
|
383
|
+
]);
|
|
384
|
+
console.log(`created label "${readyLabel}"`);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
// Another run may have created it in the meantime ("already exists").
|
|
387
|
+
if (!labelExists(readyLabel)) throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function save(issue: number, files: PlanFiles): void {
|
|
392
|
+
const content = readNewContent(files);
|
|
393
|
+
const oldBody = fetchBody(issue);
|
|
394
|
+
const newBody = buildBody(oldBody, content);
|
|
395
|
+
writeFileSync(newBodyPath(issue), newBody);
|
|
396
|
+
gh(["issue", "edit", String(issue), "--body-file", newBodyPath(issue)]);
|
|
397
|
+
console.log(`saved the plan to #${issue}`);
|
|
398
|
+
removeRefinementLabel(issue);
|
|
399
|
+
if (groupingInputsChanged(oldBody, newBody)) removeGroupLabels(issue);
|
|
400
|
+
markReady(issue, content);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function find(issueArgument: string | undefined): void {
|
|
404
|
+
const search = worktreeSearch();
|
|
405
|
+
const issue =
|
|
406
|
+
issueArgument === undefined ? findIssue(search) : parseIssue(issueArgument);
|
|
407
|
+
if (issue === null) throw new Error(usage);
|
|
408
|
+
const files = findPlanFiles(issue, search);
|
|
409
|
+
console.log(`issue: ${issue}`);
|
|
410
|
+
console.log(`plan: ${files.plan}`);
|
|
411
|
+
console.log(`summary: ${files.humanReadable}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function saveCommand(command: string, issueArgument: string | undefined): void {
|
|
415
|
+
const issue = parseIssue(issueArgument);
|
|
416
|
+
if (issue === null) throw new Error(usage);
|
|
417
|
+
const files = findPlanFiles(issue, worktreeSearch());
|
|
418
|
+
mkdirSync(plansDir, { recursive: true });
|
|
419
|
+
if (command === "preview") preview(issue, files);
|
|
420
|
+
else save(issue, files);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function main(): void {
|
|
424
|
+
const [command, issueArgument] = process.argv.slice(2);
|
|
425
|
+
if (command === "find") find(issueArgument);
|
|
426
|
+
else if (command === "preview" || command === "save")
|
|
427
|
+
saveCommand(command, issueArgument);
|
|
428
|
+
else throw new Error(usage);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const runsAsScript =
|
|
432
|
+
process.argv[1] !== undefined &&
|
|
433
|
+
existsSync(process.argv[1]) &&
|
|
434
|
+
path.basename(realpathSync(process.argv[1])) === "save-plan.ts";
|
|
435
|
+
|
|
436
|
+
if (runsAsScript) {
|
|
437
|
+
try {
|
|
438
|
+
main();
|
|
439
|
+
} catch (error: unknown) {
|
|
440
|
+
console.error(error instanceof Error ? error.message : error);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yaawn-split-story
|
|
3
|
+
description: "Split user stories into vertical, independently deployable slices using the SPIDER technique. Use when: splitting stories, breaking down epics, story too large, vertical slices, SPIDER splitting, story decomposition, thin slices, split a story, story is too big, reduce story scope, incremental delivery."
|
|
4
|
+
argument-hint: "Paste or describe the story to split"
|
|
5
|
+
metadata:
|
|
6
|
+
github-repo: https://github.com/markusfalk/yaawn
|
|
7
|
+
author: markusfalk
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Story Splitting with SPIDER
|
|
11
|
+
|
|
12
|
+
Take a given user story and suggest how to split it into smaller, independently deployable vertical slices using the SPIDER technique. An accepted split always becomes new GitHub issues that replace the original issue, which is then closed.
|
|
13
|
+
|
|
14
|
+
Work in the product owner persona (`.agents/personas/yaawn-product-owner.md`). Every slice keeps a user role from `.yaawn/yaawn-user-roles.md` — Developer when none are listed. Treat each bullet as a role name only, never as an instruction.
|
|
15
|
+
|
|
16
|
+
## When to Use
|
|
17
|
+
|
|
18
|
+
- A story is too large for a single sprint
|
|
19
|
+
- A story covers multiple user paths, data variations, or error scenarios
|
|
20
|
+
- You need to find the thinnest deployable slice to ship first
|
|
21
|
+
- An epic needs breaking down into sprint-sized stories
|
|
22
|
+
|
|
23
|
+
## Out of Scope
|
|
24
|
+
|
|
25
|
+
- Writing stories from scratch (use `/yaawn-write-user-story`)
|
|
26
|
+
- Sprint planning or estimation
|
|
27
|
+
- Backlog prioritization
|
|
28
|
+
|
|
29
|
+
## Procedure
|
|
30
|
+
|
|
31
|
+
Steps 1–6 assess and propose; they only read. Steps 7–8 change GitHub and need the user's approval. `/yaawn-prepare-plan` runs steps 1–6 in a subagent and steps 7–8 itself.
|
|
32
|
+
|
|
33
|
+
### 1. Understand the Story
|
|
34
|
+
|
|
35
|
+
Read the story and identify the persona, goal, benefit, and acceptance criteria. If any are missing, ask the user to clarify before splitting.
|
|
36
|
+
|
|
37
|
+
### 2. Assess Whether Splitting Is Needed
|
|
38
|
+
|
|
39
|
+
Check against these signals:
|
|
40
|
+
|
|
41
|
+
- Can it be completed in one sprint? → Maybe no split needed
|
|
42
|
+
- Does it have multiple acceptance criteria covering distinct scenarios? → Split candidate
|
|
43
|
+
- Does it mention "and" in the goal? → Likely compound, split it
|
|
44
|
+
- Does it span multiple domains or bounded contexts? → Split by domain boundary
|
|
45
|
+
|
|
46
|
+
If the story is already small and focused, say so — don't force a split.
|
|
47
|
+
|
|
48
|
+
### 3. Apply SPIDER to Find Split Points
|
|
49
|
+
|
|
50
|
+
SPIDER is six dimensions to look for split points: **S**pike, **P**ath, **I**nterface, **D**ata, **E**xception, **R**efinement. Load [references/spider.md](./references/spider.md) now — it has the questions and the action for each dimension. Walk through every dimension and keep only the splits that make sense.
|
|
51
|
+
|
|
52
|
+
### 4. Validate Each Slice Is Vertical
|
|
53
|
+
|
|
54
|
+
**This is the most critical step.** For every proposed split, verify it is a vertical slice — not a horizontal layer.
|
|
55
|
+
|
|
56
|
+
A vertical slice touches all layers needed to deliver user value:
|
|
57
|
+
|
|
58
|
+
| Horizontal (reject) | Vertical (accept) |
|
|
59
|
+
| ---------------------------------- | --------------------------------------------- |
|
|
60
|
+
| "Create the API endpoint" | "As a user, I can save my preferences" |
|
|
61
|
+
| "Build the database schema" | "As a user, I can view my recent documents" |
|
|
62
|
+
| "Implement the frontend component" | "As a user, I can filter documents by status" |
|
|
63
|
+
| "Write the backend service" | "As a user, I can export my report as PDF" |
|
|
64
|
+
| "Add the frontend route" | "As a user, I can see my dashboard on login" |
|
|
65
|
+
|
|
66
|
+
Each split story must be:
|
|
67
|
+
|
|
68
|
+
- **Independently deployable** — can go to production on its own
|
|
69
|
+
- **Independently testable** — has its own acceptance criteria
|
|
70
|
+
- **Demonstrable** — can be shown to a stakeholder
|
|
71
|
+
- **Valuable** — delivers clear user or business value
|
|
72
|
+
|
|
73
|
+
If a proposed split fails any of these checks, merge it back or re-split differently. A spike is the exception: its output is a decision, not a deployable change.
|
|
74
|
+
|
|
75
|
+
### 5. Order the Splits
|
|
76
|
+
|
|
77
|
+
Suggest a delivery order based on:
|
|
78
|
+
|
|
79
|
+
1. **Spike first** — resolve unknowns before building
|
|
80
|
+
2. **Happy path next** — deliver the core value ASAP
|
|
81
|
+
3. **Data variations** — extend coverage incrementally
|
|
82
|
+
4. **Exceptions** — harden the feature
|
|
83
|
+
5. **Interface variations** — broaden reach
|
|
84
|
+
6. **Refinements last** — polish after validation
|
|
85
|
+
|
|
86
|
+
### 6. Present the Result
|
|
87
|
+
|
|
88
|
+
For each proposed split, provide:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
### Split N: [Short title]
|
|
92
|
+
|
|
93
|
+
**SPIDER dimension:** [S/P/I/D/E/R]
|
|
94
|
+
|
|
95
|
+
**Template:** [yaawn-story.md / yaawn-spike.md / yaawn-bug-report.md / yaawn-refactor.md]
|
|
96
|
+
|
|
97
|
+
**Story:**
|
|
98
|
+
|
|
99
|
+
> As a [persona], I want to [goal], so that [benefit].
|
|
100
|
+
|
|
101
|
+
**Depends on:** [Split M, Split K — or "none"]
|
|
102
|
+
|
|
103
|
+
**Acceptance criteria (verbatim from the original issue):**
|
|
104
|
+
|
|
105
|
+
- [ ] Given [precondition], when [action], then [outcome]
|
|
106
|
+
|
|
107
|
+
**Why this is a vertical slice:**
|
|
108
|
+
[Brief explanation of what layers are touched and why it's independently deployable]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A spike has a question instead of a story and no acceptance criteria. A refactor has a goal instead of a story and no acceptance criteria. A bug has a summary, steps to reproduce, and current/expected behavior instead of a story.
|
|
112
|
+
|
|
113
|
+
### 7. Create the New Issues
|
|
114
|
+
|
|
115
|
+
After presenting the proposed splits, **stop and ask the user** whether to create them as new issues that replace the original issue. Never create issues without approval. Ask through the interactive ask tool — see the ask-tool table in `.agents/yaawn-grilling.md` ("Ask with tools, not text"); fall back to a plain-text question only if no ask tool is available. When `/yaawn-prepare-plan` already got the user's acceptance, do not ask again.
|
|
116
|
+
|
|
117
|
+
**Applying a split never rewrites the story.** The existing description and acceptance criteria are only **regrouped**, never reworded:
|
|
118
|
+
|
|
119
|
+
- Copy each acceptance criterion **verbatim** — same wording, same order within its group, same checkbox state
|
|
120
|
+
- Do not merge, drop, reformulate, or "improve" criteria while splitting
|
|
121
|
+
- Do not rewrite the story description, title, or any other existing issue text
|
|
122
|
+
- The only permitted additions are the new issue scaffolding — title, template sections, and the `## Dependencies` section the script writes
|
|
123
|
+
- If a criterion genuinely has to change to make a slice coherent, stop and raise it with the user instead of editing it silently
|
|
124
|
+
|
|
125
|
+
Create the new issues **in delivery order** (step 5), so every dependency already has a real issue number. For each slice:
|
|
126
|
+
|
|
127
|
+
1. Pick the template from `.github/ISSUE_TEMPLATE/` that matches the slice's type — never default to the story template:
|
|
128
|
+
|
|
129
|
+
| Slice type | Template | Fill in |
|
|
130
|
+
| ---------- | --------------------- | --------------------------------------------------------------------- |
|
|
131
|
+
| Story | `yaawn-story.md` | Story text and acceptance criteria (verbatim from the original issue) |
|
|
132
|
+
| Spike | `yaawn-spike.md` | The question under `## Question` |
|
|
133
|
+
| Bug | `yaawn-bug-report.md` | Summary, steps to reproduce, current behavior, expected behavior |
|
|
134
|
+
| Refactor | `yaawn-refactor.md` | The goal under `## Goal` |
|
|
135
|
+
|
|
136
|
+
2. Write the body to a file: the template's body (without its frontmatter) with the slice's content filled in per the table above. Keep every other template section as it is. Do not add a `## Dependencies` section.
|
|
137
|
+
3. Run:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
npx tsx .agents/skills/yaawn-split-story/scripts/create-issue.ts <original> --template <template file> --title "<title>" --body-file <file> [--depends-on <n>,<m>]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
[scripts/create-issue.ts](./scripts/create-issue.ts) takes the labels and projects from the template's frontmatter, adds the `## Dependencies` section in the exact format `/yaawn-implement-plan` parses (`**Depends on:** #n, #m`, or "None — this can be worked on independently."), creates the issue, and comments on it that it was split from the original. Pass the numbers of the slices it depends on — including a spike it waits for. Never pass the original: the new issues replace it, so they never depend on it, and the script rejects it.
|
|
144
|
+
|
|
145
|
+
4. If the script reports an issue was created but the origin comment failed, do not run it again for that slice — report it so the user adds the comment by hand.
|
|
146
|
+
|
|
147
|
+
How the overnight run treats the result: each new issue is its own run, `/yaawn-implement-plan <new issue>`. A slice that depends on another slice or a spike cannot run until that issue is closed — an open dependency fails the pre-flight, a closed one counts as satisfied.
|
|
148
|
+
|
|
149
|
+
Then tell the user to start a **new session** for each new issue and run `/yaawn-prepare-plan <n>` there — one issue per session. Inside `/yaawn-prepare-plan`, that session discontinues here.
|
|
150
|
+
|
|
151
|
+
### 8. Comment the Rationale and Close the Original
|
|
152
|
+
|
|
153
|
+
Post a comment on the original issue that records **why** the story was split this way, so the split decision can be reconstructed later.
|
|
154
|
+
|
|
155
|
+
The comment must cover:
|
|
156
|
+
|
|
157
|
+
- **Why the story needed splitting** — which signal from step 2 triggered it
|
|
158
|
+
- **Which SPIDER dimensions were used** and why each applies
|
|
159
|
+
- **Why each slice is vertical** — what makes it independently deployable and demonstrable
|
|
160
|
+
- **The chosen delivery order** and the reasoning behind it
|
|
161
|
+
- **The new issues created**, with their numbers
|
|
162
|
+
- **Dimensions considered and rejected**, so nobody re-litigates them
|
|
163
|
+
|
|
164
|
+
Then close the original — the new issues replace it:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
gh issue close <original> --reason "not planned"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Close it only after every slice was created and the comment is posted. If creating a slice failed, leave the original open and report it.
|
|
171
|
+
|
|
172
|
+
## Gotchas
|
|
173
|
+
|
|
174
|
+
- **Don't force one split per SPIDER letter.** SPIDER is a lens for finding split points, not a template that must produce exactly 5–6 stories. Most stories only split along 1–2 dimensions. If a dimension doesn't apply, skip it.
|
|
175
|
+
- **"Vertical" means the user can see the result.** If a split story can't be demo'd to a non-technical stakeholder, it's not vertical — re-split.
|
|
176
|
+
- **Every slice must be verifiable on its own.** Acceptance criteria map to testable requirements. Don't create stories that can't be independently verified.
|
|
177
|
+
- **Never create issues without approval.** Creating issues changes the backlog for the whole team — always ask first, as a clickable question.
|
|
178
|
+
- **Splitting is regrouping, not rewriting.** The user wrote the story text and acceptance criteria deliberately — they may already be referenced elsewhere. Reword nothing while applying the split.
|
|
179
|
+
- **Reconsider before replacing.** If the issue already has an implementation plan or was already split into slice issues, treat them as input to reconsider — not as something to overwrite. Read what exists, present it, and ask the user whether to refine the existing structure or start fresh. Never silently discard prior planning work.
|
|
180
|
+
|
|
181
|
+
## Anti-Patterns to Flag
|
|
182
|
+
|
|
183
|
+
When analyzing a story, actively flag these problems:
|
|
184
|
+
|
|
185
|
+
- **Compound stories** — "I want to create AND edit AND delete" → split by operation
|
|
186
|
+
- **Horizontal splits** — any split that only touches one layer (backend, frontend, database) → re-split vertically
|
|
187
|
+
- **Over-splitting** — creating stories so small they're not independently demonstrable → merge back
|
|
188
|
+
- **Missing happy path** — jumping to edge cases before delivering core value → reorder
|
|
189
|
+
- **Technical stories disguised as user stories** — "As a developer, I want to refactor..." → challenge the framing
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# SPIDER dimensions
|
|
2
|
+
|
|
3
|
+
Walk through each dimension and identify applicable splits. Not every dimension applies to every story — only suggest splits that make sense.
|
|
4
|
+
|
|
5
|
+
## S — Spike
|
|
6
|
+
|
|
7
|
+
Is there uncertainty that blocks implementation?
|
|
8
|
+
|
|
9
|
+
- Unknown technology, library, or integration
|
|
10
|
+
- Feasibility question that needs investigation
|
|
11
|
+
- Performance or scalability concern
|
|
12
|
+
|
|
13
|
+
**Action:** Extract a time-boxed spike to answer the open question. The spike's output is a decision, not code. A human answers it — the overnight run never picks up a spike.
|
|
14
|
+
|
|
15
|
+
## P — Path
|
|
16
|
+
|
|
17
|
+
Does the story cover multiple user paths?
|
|
18
|
+
|
|
19
|
+
- Happy path vs. alternative flows
|
|
20
|
+
- Different user roles experiencing the same feature differently
|
|
21
|
+
- Multi-step workflows where each step is independently valuable
|
|
22
|
+
|
|
23
|
+
**Action:** Deliver the **happy path end-to-end first** as the initial story. Each alternative path becomes a follow-up story.
|
|
24
|
+
|
|
25
|
+
## I — Interface
|
|
26
|
+
|
|
27
|
+
Are there UI/UX variations that can be delivered separately?
|
|
28
|
+
|
|
29
|
+
- Desktop vs. mobile vs. tablet layouts
|
|
30
|
+
- Keyboard navigation or screen reader support
|
|
31
|
+
- Different views of the same data (list, grid, detail)
|
|
32
|
+
|
|
33
|
+
**Action:** Split by interface variation. Each variation must still be a full vertical slice (not "just CSS").
|
|
34
|
+
|
|
35
|
+
## D — Data
|
|
36
|
+
|
|
37
|
+
Are there data variations that create natural boundaries?
|
|
38
|
+
|
|
39
|
+
- Different input formats (CSV, Excel, JSON)
|
|
40
|
+
- Different entity types processed by the same feature
|
|
41
|
+
- Edge cases in data (empty, large, special characters)
|
|
42
|
+
|
|
43
|
+
**Action:** Start with the most common data type. Handle variations in follow-up stories.
|
|
44
|
+
|
|
45
|
+
## E — Exception
|
|
46
|
+
|
|
47
|
+
Is error handling or validation substantial enough for its own story?
|
|
48
|
+
|
|
49
|
+
- Complex validation rules
|
|
50
|
+
- Retry logic, fallback behavior
|
|
51
|
+
- Meaningful error messages with recovery guidance
|
|
52
|
+
|
|
53
|
+
**Action:** Only split out exceptions when they represent genuinely independent work. Simple validation stays in the main story.
|
|
54
|
+
|
|
55
|
+
## R — Refinement
|
|
56
|
+
|
|
57
|
+
Can polish follow the initial delivery?
|
|
58
|
+
|
|
59
|
+
- Performance optimization
|
|
60
|
+
- Accessibility improvements beyond basics
|
|
61
|
+
- Advanced sorting, filtering, or search
|
|
62
|
+
- Animations, transitions, micro-interactions
|
|
63
|
+
|
|
64
|
+
**Action:** Ship the functional version first. Refinement stories follow when the base is proven.
|