@yaawn/workflow 0.3.1 → 0.5.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.
Files changed (96) hide show
  1. package/README.md +22 -43
  2. package/agents/personas/yaawn-product-owner.md +4 -4
  3. package/agents/skills/yaawn-create-plan/SKILL.md +83 -50
  4. package/agents/skills/yaawn-extract-learnings/SKILL.md +138 -0
  5. package/agents/skills/yaawn-grill-bug-report/SKILL.md +7 -0
  6. package/agents/skills/yaawn-grill-other/SKILL.md +7 -0
  7. package/agents/skills/yaawn-grill-prototype/SKILL.md +38 -0
  8. package/agents/skills/yaawn-grill-prototype/references/prototype-checklist.md +23 -0
  9. package/agents/skills/yaawn-grill-refactor/SKILL.md +7 -0
  10. package/agents/skills/yaawn-grill-spike/SKILL.md +7 -0
  11. package/agents/skills/yaawn-grill-story/SKILL.md +8 -1
  12. package/agents/skills/yaawn-grill-story/references/concept-challenge.md +12 -2
  13. package/agents/skills/yaawn-grill-story/references/story-checklist.md +2 -1
  14. package/agents/skills/yaawn-implement-plan/SKILL.md +10 -8
  15. package/agents/skills/yaawn-implement-plan/scripts/agent.ts +1 -1
  16. package/agents/skills/yaawn-implement-plan/scripts/overnight.ts +7 -4
  17. package/agents/skills/yaawn-implement-plan/scripts/parallel.ts +155 -12
  18. package/agents/skills/yaawn-implement-plan/scripts/processes.ts +161 -1
  19. package/agents/skills/yaawn-implement-plan/scripts/targets.ts +13 -2
  20. package/agents/skills/yaawn-implement-plan/scripts/worktrees.ts +219 -52
  21. package/agents/skills/yaawn-prepare-plan/SKILL.md +20 -19
  22. package/agents/skills/yaawn-save-plan/SKILL.md +26 -7
  23. package/agents/skills/yaawn-save-plan/scripts/plan-body.ts +75 -9
  24. package/agents/skills/yaawn-save-plan/scripts/save-plan.ts +97 -1
  25. package/agents/skills/yaawn-split-story/SKILL.md +1 -1
  26. package/agents/skills/yaawn-verify-executed-plan/SKILL.md +28 -13
  27. package/agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts +44 -12
  28. package/agents/skills/yaawn-write-bug-report/SKILL.md +6 -5
  29. package/agents/skills/yaawn-write-other/SKILL.md +6 -5
  30. package/agents/skills/yaawn-write-prototype/SKILL.md +96 -0
  31. package/agents/skills/yaawn-write-refactor/SKILL.md +6 -5
  32. package/agents/skills/yaawn-write-spike/SKILL.md +1 -1
  33. package/agents/skills/yaawn-write-user-story/SKILL.md +4 -3
  34. package/agents/yaawn-glossary.md +6 -5
  35. package/config/install-tasks.json +0 -1
  36. package/config/wizard.json +5 -10
  37. package/dist/cli/args.js +66 -0
  38. package/dist/cli/errors.js +103 -0
  39. package/dist/cli/exit-codes.js +3 -0
  40. package/dist/cli/index.js +11 -0
  41. package/dist/cli/install-report.js +92 -0
  42. package/dist/cli/install.js +124 -0
  43. package/dist/cli/main.js +17 -0
  44. package/dist/cli/package-root.js +17 -0
  45. package/dist/cli/project-root.js +11 -0
  46. package/dist/cli/run.js +141 -0
  47. package/dist/cli/session.js +1 -0
  48. package/dist/cli/terminal.js +5 -0
  49. package/dist/cli/uninstall.js +89 -0
  50. package/dist/cli/version.js +7 -0
  51. package/dist/config/agents.js +108 -0
  52. package/dist/config/config.js +118 -0
  53. package/dist/config/errors.js +45 -0
  54. package/dist/config/index.js +11 -0
  55. package/dist/config/manifest.js +28 -0
  56. package/dist/config/messages.js +86 -0
  57. package/dist/config/names.js +40 -0
  58. package/dist/config/paths.js +73 -0
  59. package/dist/config/settings.js +101 -0
  60. package/dist/config/strict-json.js +119 -0
  61. package/dist/config/tasks.js +77 -0
  62. package/dist/config/wizard.js +139 -0
  63. package/dist/display/index.js +3 -0
  64. package/dist/display/palette.js +61 -0
  65. package/dist/display/renderer.js +169 -0
  66. package/dist/display/report.js +15 -0
  67. package/dist/display/sanitize.js +10 -0
  68. package/dist/link/errors.js +34 -0
  69. package/dist/link/fs-utils.js +79 -0
  70. package/dist/link/index.js +4 -0
  71. package/dist/link/install.js +20 -0
  72. package/dist/link/linker.js +137 -0
  73. package/dist/link/memory.js +221 -0
  74. package/dist/link/names.js +30 -0
  75. package/dist/link/types.js +30 -0
  76. package/dist/link/uninstall.js +90 -0
  77. package/dist/sync/apply.js +74 -0
  78. package/dist/sync/changes.js +31 -0
  79. package/dist/sync/fs-utils.js +63 -0
  80. package/dist/sync/index.js +7 -0
  81. package/dist/sync/install.js +57 -0
  82. package/dist/sync/languages.js +80 -0
  83. package/dist/sync/plan.js +146 -0
  84. package/dist/sync/projects.js +294 -0
  85. package/dist/sync/uninstall.js +234 -0
  86. package/dist/wizard/build.js +89 -0
  87. package/dist/wizard/index.js +5 -0
  88. package/dist/wizard/items.js +83 -0
  89. package/dist/wizard/prompts.js +167 -0
  90. package/dist/wizard/section.js +143 -0
  91. package/dist/wizard/wizard.js +73 -0
  92. package/github/ISSUE_TEMPLATE/yaawn-prototype.md +48 -0
  93. package/package.json +11 -10
  94. package/yaawn/yaawn-user-roles.md +1 -1
  95. package/agents/instructions/output-styles/yaawn-eli5.instructions.md +0 -45
  96. package/scripts/install.js +0 -73
@@ -1,19 +1,25 @@
1
1
  import { execFile, execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
2
3
  import path from "node:path";
3
4
  import { promisify } from "node:util";
4
5
  import { uncommittedChangesAsync } from "./targets.js";
5
6
 
6
7
  // Shared by yaawn-implement-plan (creates one git worktree per issue) and
7
- // yaawn-verify-executed-plan (checks it verifies in the issue's worktree).
8
+ // yaawn-verify-executed-plan (resolves the issue's worktree to verify in).
8
9
  //
9
10
  // Layout: the project root holds a bare `.git` repository, the main working
10
11
  // copy is a normal checkout at `<project>/main`, and every issue worktree is
11
- // a sibling of it at `<project>/yaawn-<issue>-<slug>`.
12
+ // a sibling of it at `<project>/yaawn-<issue>-<slug>`. A prototype issue
13
+ // (labelled `Yaawn: Prototype`) gets its worktree at
14
+ // `<project>/prototype/yaawn-<issue>-<slug>` on branch
15
+ // `prototype/yaawn-<issue>-<slug>`, which is never merged.
12
16
 
13
17
  export type WorktreeCheckStatus = "ok" | "warn" | "fail";
14
18
 
19
+ // `path` is the worktree to verify in; null only when the check fails.
15
20
  export interface WorktreeCheck {
16
21
  status: WorktreeCheckStatus;
22
+ path: string | null;
17
23
  message: string;
18
24
  }
19
25
 
@@ -21,16 +27,20 @@ export interface WorktreeLayout {
21
27
  issue: number;
22
28
  mainCopy: string;
23
29
  registered: Iterable<string>;
24
- currentToplevel: string;
25
- // Resolved by branch (yaawn/<issue>), not by the title slug — the slug in
26
- // the directory name is for humans only.
30
+ // Resolved by branch (yaawn/<issue>, or prototype/yaawn-<issue>-*), not by
31
+ // the title-derived path.
27
32
  ownWorktree?: string | null;
33
+ locked?: Iterable<string>;
34
+ // Registered, but git marks them prunable or their directory is gone.
35
+ stale?: Iterable<string>;
28
36
  }
29
37
 
30
38
  export interface WorktreeEntry {
31
39
  path: string;
32
40
  branch: string | null;
33
41
  bare: boolean;
42
+ locked: boolean;
43
+ prunable: boolean;
34
44
  }
35
45
 
36
46
  export interface AheadBehind {
@@ -58,12 +68,14 @@ const branchPrefix = "branch refs/heads/";
58
68
  const maxSlugLength = 40;
59
69
  const fallbackMainCopyName = "main";
60
70
  const fallbackDefaultBranches = ["main", "master"];
71
+ const prototypeDir = "prototype";
72
+ const fallbackPrototypeSlug = "prototype";
73
+
74
+ export const prototypeLabel = "Yaawn: Prototype";
61
75
 
62
76
  const aheadBehindPattern = /^(\d+)\s+(\d+)$/;
63
77
  const execFileAsync = promisify(execFile);
64
78
 
65
- export const branchName = (issue: number): string => `yaawn/${issue}`;
66
-
67
79
  export function git(args: string[]): string {
68
80
  return execFileSync("git", args, {
69
81
  encoding: "utf-8",
@@ -92,9 +104,37 @@ export function titleSlug(title: string): string {
92
104
  .replace(/-+$/, "");
93
105
  }
94
106
 
95
- export function worktreeName(issue: number, title: string): string {
107
+ // Exact label-name match only: a look-alike label never makes a prototype.
108
+ export const isPrototype = (labels: readonly string[]): boolean =>
109
+ labels.includes(prototypeLabel);
110
+
111
+ // A prototype branch always carries a slug, so `prototype/yaawn-<issue>-*`
112
+ // finds it.
113
+ const prototypeSlug = (title: string): string =>
114
+ titleSlug(title) || fallbackPrototypeSlug;
115
+
116
+ export function prototypeBranchPrefix(issue: number): string {
96
117
  assertIssueNumber(issue);
97
- const slug = titleSlug(title);
118
+ return `${prototypeDir}/yaawn-${issue}-`;
119
+ }
120
+
121
+ export function branchName(
122
+ issue: number,
123
+ labels: readonly string[] = [],
124
+ title = "",
125
+ ): string {
126
+ return isPrototype(labels)
127
+ ? `${prototypeBranchPrefix(issue)}${prototypeSlug(title)}`
128
+ : `yaawn/${issue}`;
129
+ }
130
+
131
+ export function worktreeName(
132
+ issue: number,
133
+ title: string,
134
+ labels: readonly string[] = [],
135
+ ): string {
136
+ assertIssueNumber(issue);
137
+ const slug = isPrototype(labels) ? prototypeSlug(title) : titleSlug(title);
98
138
  return slug ? `yaawn-${issue}-${slug}` : `yaawn-${issue}`;
99
139
  }
100
140
 
@@ -109,6 +149,8 @@ function entryFromBlock(lines: string[]): WorktreeEntry | null {
109
149
  branch:
110
150
  branchLine === undefined ? null : branchLine.slice(branchPrefix.length),
111
151
  bare: lines.includes("bare"),
152
+ locked: lines.some((line) => line === "locked" || line.startsWith("locked ")),
153
+ prunable: lines.some((line) => line === "prunable" || line.startsWith("prunable ")),
112
154
  };
113
155
  }
114
156
 
@@ -163,12 +205,56 @@ export function branchWorktreeFromPorcelain(
163
205
  return checkout?.path ?? null;
164
206
  }
165
207
 
208
+ // A prototype's checkout on any `prototype/yaawn-<issue>-*` branch counts, so
209
+ // a relaunch resumes it even after the issue was renamed.
210
+ export function issueWorktreeFromPorcelain(
211
+ porcelain: string,
212
+ issue: number,
213
+ labels: readonly string[] = [],
214
+ title = "",
215
+ ): string | null {
216
+ const exact = branchWorktreeFromPorcelain(
217
+ porcelain,
218
+ branchName(issue, labels, title),
219
+ );
220
+ if (exact !== null || !isPrototype(labels)) return exact;
221
+ const prefix = prototypeBranchPrefix(issue);
222
+ const checkout = entriesFromPorcelain(porcelain).find(
223
+ (entry) => !entry.bare && entry.branch?.startsWith(prefix) === true,
224
+ );
225
+ return checkout?.path ?? null;
226
+ }
227
+
228
+ // Both the worktree path and its actual branch, so a relaunch after a
229
+ // rename keeps reporting the branch that is really checked out.
230
+ export function issueWorktreeAndBranchFromPorcelain(
231
+ porcelain: string,
232
+ issue: number,
233
+ labels: readonly string[] = [],
234
+ title = "",
235
+ ): { path: string; branch: string } | null {
236
+ const entry = entriesFromPorcelain(porcelain).find((candidate) => {
237
+ if (candidate.bare || candidate.branch === null) return false;
238
+ if (candidate.branch === branchName(issue, labels, title)) return true;
239
+ return (
240
+ isPrototype(labels) &&
241
+ candidate.branch.startsWith(prototypeBranchPrefix(issue))
242
+ );
243
+ });
244
+ if (entry === undefined || entry.branch === null) return null;
245
+ return { path: entry.path, branch: entry.branch };
246
+ }
247
+
166
248
  export function worktreePathFor(
167
249
  projectRoot: string,
168
250
  issue: number,
169
251
  title: string,
252
+ labels: readonly string[] = [],
170
253
  ): string {
171
- return path.join(projectRoot, worktreeName(issue, title));
254
+ const name = worktreeName(issue, title, labels);
255
+ return isPrototype(labels)
256
+ ? path.join(projectRoot, prototypeDir, name)
257
+ : path.join(projectRoot, name);
172
258
  }
173
259
 
174
260
  // --- git
@@ -209,22 +295,58 @@ export function usesBareLayout(): boolean {
209
295
  return isBareLayout(listPorcelain());
210
296
  }
211
297
 
212
- export function worktreePath(issue: number, title: string): string {
298
+ export function worktreePath(
299
+ issue: number,
300
+ title: string,
301
+ labels: readonly string[] = [],
302
+ ): string {
213
303
  return worktreePathFor(
214
304
  projectRootFromPorcelain(listPorcelain()),
215
305
  issue,
216
306
  title,
307
+ labels,
217
308
  );
218
309
  }
219
310
 
220
311
  // The branch's existing checkout wins over the title-derived path, so a
221
312
  // relaunch resumes the earlier worktree even after the issue was renamed.
222
- export function laneWorktreePath(issue: number, title: string): string {
313
+ export function laneWorktreePath(
314
+ issue: number,
315
+ title: string,
316
+ labels: readonly string[] = [],
317
+ ): string {
223
318
  const porcelain = listPorcelain();
224
319
  return (
225
- branchWorktreeFromPorcelain(porcelain, branchName(issue)) ??
226
- worktreePathFor(projectRootFromPorcelain(porcelain), issue, title)
320
+ issueWorktreeFromPorcelain(porcelain, issue, labels, title) ??
321
+ worktreePathFor(projectRootFromPorcelain(porcelain), issue, title, labels)
322
+ );
323
+ }
324
+
325
+ // Resolves both the worktree path and branch for a lane: the existing
326
+ // checkout's actual branch when the worktree is reused, otherwise the
327
+ // title-derived values for a fresh worktree.
328
+ export function laneWorktreeAndBranch(
329
+ issue: number,
330
+ title: string,
331
+ labels: readonly string[] = [],
332
+ ): { path: string; branch: string } {
333
+ const porcelain = listPorcelain();
334
+ const existing = issueWorktreeAndBranchFromPorcelain(
335
+ porcelain,
336
+ issue,
337
+ labels,
338
+ title,
227
339
  );
340
+ if (existing !== null) return existing;
341
+ return {
342
+ path: worktreePathFor(
343
+ projectRootFromPorcelain(porcelain),
344
+ issue,
345
+ title,
346
+ labels,
347
+ ),
348
+ branch: branchName(issue, labels, title),
349
+ };
228
350
  }
229
351
 
230
352
  export function registeredWorktrees(): Set<string> {
@@ -236,58 +358,96 @@ const normalized = (worktree: string): string => path.resolve(worktree);
236
358
  const samePath = (left: string, right: string): boolean =>
237
359
  normalized(left) === normalized(right);
238
360
 
239
- export function runsInOwnWorktree(issue: number, title: string): boolean {
240
- return samePath(worktreePath(issue, title), currentToplevel());
361
+ export function runsInOwnWorktree(
362
+ issue: number,
363
+ title: string,
364
+ labels: readonly string[] = [],
365
+ ): boolean {
366
+ return samePath(worktreePath(issue, title, labels), currentToplevel());
241
367
  }
242
368
 
243
369
  // --- classification
244
370
 
245
- function checkOwnWorktree(own: string, toplevel: string): WorktreeCheck {
246
- return samePath(own, toplevel)
247
- ? { status: "ok", message: `verifying in the issue's worktree ${own}` }
248
- : {
249
- status: "fail",
250
- message: `run verification in the issue's worktree ${own}, not in ${toplevel}`,
251
- };
371
+ const includesPath = (paths: Iterable<string> = [], target: string): boolean =>
372
+ [...paths].some((candidate) => samePath(candidate, target));
373
+
374
+ const failedCheck = (message: string): WorktreeCheck => ({
375
+ status: "fail",
376
+ path: null,
377
+ message,
378
+ });
379
+
380
+ function checkOwnWorktree(layout: WorktreeLayout, own: string): WorktreeCheck {
381
+ const { issue, locked, stale } = layout;
382
+ if (includesPath(stale, own)) {
383
+ return failedCheck(
384
+ `#${issue}'s worktree ${own} is stale — its directory is missing; restore it or run \`git worktree prune\``,
385
+ );
386
+ }
387
+ if (includesPath(locked, own)) {
388
+ return failedCheck(
389
+ `#${issue}'s worktree ${own} is locked — unlock it with \`git worktree unlock\` first`,
390
+ );
391
+ }
392
+ return { status: "ok", path: own, message: `verifying in the issue's worktree ${own}` };
252
393
  }
253
394
 
254
- function checkMainCopy(
255
- issue: number,
256
- mainCopy: string,
257
- toplevel: string,
258
- ): WorktreeCheck {
259
- return samePath(mainCopy, toplevel)
260
- ? {
261
- status: "warn",
262
- message: `#${issue} has no git worktree of its own — verifying the main copy`,
263
- }
264
- : {
265
- status: "fail",
266
- message: `#${issue} has no git worktree of its own — run verification in the main copy ${mainCopy}, not in ${toplevel}`,
267
- };
395
+ function checkMainCopy(layout: WorktreeLayout): WorktreeCheck {
396
+ const { issue, mainCopy, registered, stale } = layout;
397
+ if (!includesPath(registered, mainCopy) || includesPath(stale, mainCopy)) {
398
+ return failedCheck(
399
+ `#${issue} has no git worktree of its own, and the main copy ${mainCopy} is missing`,
400
+ );
401
+ }
402
+ return {
403
+ status: "warn",
404
+ path: mainCopy,
405
+ message: `#${issue} has no git worktree of its own — verifying the main copy`,
406
+ };
268
407
  }
269
408
 
409
+ // Resolves where to verify, wherever the session runs.
270
410
  export function classifyWorktree(layout: WorktreeLayout): WorktreeCheck {
271
- const { issue, mainCopy, registered, ownWorktree, currentToplevel } = layout;
272
- const own =
273
- ownWorktree != null &&
274
- [...registered].some((worktree) => samePath(worktree, ownWorktree))
275
- ? ownWorktree
276
- : null;
277
- return own !== null
278
- ? checkOwnWorktree(own, currentToplevel)
279
- : checkMainCopy(issue, mainCopy, currentToplevel);
411
+ const { registered, ownWorktree } = layout;
412
+ return ownWorktree != null && includesPath(registered, ownWorktree)
413
+ ? checkOwnWorktree(layout, ownWorktree)
414
+ : checkMainCopy(layout);
415
+ }
416
+
417
+ function staleWorktrees(entries: WorktreeEntry[]): string[] {
418
+ return entries
419
+ .filter((entry) => entry.prunable || !existsSync(entry.path))
420
+ .map((entry) => entry.path);
280
421
  }
281
422
 
282
- export function checkWorktree(issue: number): WorktreeCheck {
423
+ function layoutFromGit(
424
+ issue: number,
425
+ labels: readonly string[],
426
+ title: string,
427
+ ): WorktreeLayout {
283
428
  const porcelain = listPorcelain();
284
- return classifyWorktree({
429
+ const entries = entriesFromPorcelain(porcelain);
430
+ return {
285
431
  issue,
286
432
  mainCopy: mainCopyFromPorcelain(porcelain, defaultBranches()),
287
- registered: worktreesFromPorcelain(porcelain),
288
- currentToplevel: currentToplevel(),
289
- ownWorktree: branchWorktreeFromPorcelain(porcelain, branchName(issue)),
290
- });
433
+ registered: entries.map((entry) => entry.path),
434
+ ownWorktree: issueWorktreeFromPorcelain(porcelain, issue, labels, title),
435
+ locked: entries.filter((entry) => entry.locked).map((entry) => entry.path),
436
+ stale: staleWorktrees(entries),
437
+ };
438
+ }
439
+
440
+ // Never throws: a git failure becomes a failed check.
441
+ export function checkWorktree(
442
+ issue: number,
443
+ labels: readonly string[] = [],
444
+ title = "",
445
+ ): WorktreeCheck {
446
+ try {
447
+ return classifyWorktree(layoutFromGit(issue, labels, title));
448
+ } catch (error) {
449
+ return failedCheck(`could not resolve the worktree: ${gitErrorReason(error)}`);
450
+ }
291
451
  }
292
452
 
293
453
  // --- main copy sync
@@ -338,6 +498,13 @@ function redactedGitError(error: unknown): string {
338
498
  return line ? `: ${line.replace(/\/\/[^/\s@]+@/g, "//***@")}` : "";
339
499
  }
340
500
 
501
+ function gitErrorReason(error: unknown): string {
502
+ const stderr = redactedGitError(error);
503
+ if (stderr) return stderr.slice(": ".length);
504
+ const message = error instanceof Error ? error.message : String(error);
505
+ return message.split("\n")[0];
506
+ }
507
+
341
508
  // Never waits for a credential prompt: an unattended launch would hang.
342
509
  async function gitAsync(args: string[]): Promise<string> {
343
510
  const { stdout } = await execFileAsync("git", args, {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yaawn-prepare-plan
3
- description: "Turn a new idea or an existing GitHub issue into an approved work item — a story, refactor, spike, bug report, or other change — pick or confirm its type, draft or reconsider it, grill it, and save it to the issue — ready for /yaawn-create-plan (spikes go to a human instead). Use when: preparing work for planning, new idea, refine issue #N, write and grill a story, write a refactor, write a spike, write a bug report, write an other change, check if a story needs splitting, get a work item ready for planning."
3
+ description: "Turn a new idea or an existing GitHub issue into an approved work item — a story, refactor, spike, bug report, prototype, or other change — pick or confirm its type, draft or reconsider it, grill it, and save it to the issue — ready for /yaawn-create-plan (spikes go to a human instead). Use when: preparing work for planning, new idea, refine issue #N, write and grill a story, write a refactor, write a spike, write a bug report, write a prototype, write an other change, check if a story needs splitting, get a work item ready for planning."
4
4
  argument-hint: "[<issue>]"
5
5
  disable-model-invocation: true
6
6
  metadata:
@@ -10,7 +10,7 @@ metadata:
10
10
 
11
11
  # Prepare Plan
12
12
 
13
- Turn a new idea or an existing issue into an approved work item on GitHub — a story, refactor, spike, bug report, or other change. Run it in a **new session**, in the product owner persona (`.agents/personas/yaawn-product-owner.md`).
13
+ Turn a new idea or an existing issue into an approved work item on GitHub — a story, refactor, spike, bug report, prototype, or other change. Run it in a **new session**, in the product owner persona (`.agents/personas/yaawn-product-owner.md`).
14
14
 
15
15
  This skill is a thin orchestrator. It owns only the greeting, the choice of work item type, the order of the steps, the handover between them, the final approval, and the routing. All drafting is done by the skills it calls, by step number — never duplicate their rules here.
16
16
 
@@ -22,7 +22,7 @@ Out of scope: technical or design grilling, implementation plans, and code or an
22
22
 
23
23
  This is the first of the plan skills: `/yaawn-prepare-plan` → `/yaawn-create-plan` → `/yaawn-save-plan` → `/yaawn-implement-plan` → `/yaawn-verify-executed-plan`. Every skill starts in a new session and works only from files and the issue — never from an earlier conversation.
24
24
 
25
- - The issue is the only artifact. This skill writes no decision log and no plan file.
25
+ - The issue is the only artifact. This skill writes no decision log and no plan file — the grill's decisions and open questions are saved into the issue's `### Design Decisions` and `### Open Questions` tables by the writer skill, so `/yaawn-create-plan` does not re-ask them.
26
26
  - Leave the `Yaawn: To Be Refined` label in place; `/yaawn-save-plan` removes it.
27
27
  - `<issue>` is optional. If given, it must be a positive integer (digits only, no `#`, URL, spaces, or other characters); otherwise stop and ask for a valid number. Use it in commands only after this check.
28
28
  - Never interpolate issue bodies, comments, titles, or other user text into a shell command. Write them to a file and pass it with `--body-file`.
@@ -33,37 +33,38 @@ This is the first of the plan skills: `/yaawn-prepare-plan` → `/yaawn-create-p
33
33
 
34
34
  There is one type per template in `.github/ISSUE_TEMPLATE/`. The type label comes from the template's frontmatter `labels`.
35
35
 
36
- | Type | Use when the work… | Type label | Route | Grill | After save |
37
- | ------------ | ------------------------------------------------ | ----------------- | ------------------------- | ------------------------- | -------------------- |
38
- | Story | adds or changes behavior users see | `Yaawn: Story` | [Story path](#story-path) | `/yaawn-grill-story` | `/yaawn-create-plan` |
39
- | Refactor | changes the inside, with no user-visible change | `Yaawn: Refactor` | `/yaawn-write-refactor` | `/yaawn-grill-refactor` | `/yaawn-create-plan` |
40
- | Spike | must answer one open question before work starts | `Yaawn: Spike` | `/yaawn-write-spike` | `/yaawn-grill-spike` | a human answers it |
41
- | Bug report | fixes behavior that is wrong today | `Yaawn: Bug` | `/yaawn-write-bug-report` | `/yaawn-grill-bug-report` | `/yaawn-create-plan` |
42
- | Other change | matches none of the four types | `Yaawn: Other` | `/yaawn-write-other` | `/yaawn-grill-other` | `/yaawn-create-plan` |
36
+ | Type | Use when the work… | Type label | Route | Grill | After save |
37
+ | ------------ | ------------------------------------------------- | ------------------ | ------------------------- | ------------------------- | -------------------- |
38
+ | Story | adds or changes behavior users see | `Yaawn: Story` | [Story path](#story-path) | `/yaawn-grill-story` | `/yaawn-create-plan` |
39
+ | Refactor | changes the inside, with no user-visible change | `Yaawn: Refactor` | `/yaawn-write-refactor` | `/yaawn-grill-refactor` | `/yaawn-create-plan` |
40
+ | Spike | must answer one open question before work starts | `Yaawn: Spike` | `/yaawn-write-spike` | `/yaawn-grill-spike` | a human answers it |
41
+ | Bug report | fixes behavior that is wrong today | `Yaawn: Bug` | `/yaawn-write-bug-report` | `/yaawn-grill-bug-report` | `/yaawn-create-plan` |
42
+ | Prototype | builds a throwaway version to test with customers | `Yaawn: Prototype` | `/yaawn-write-prototype` | `/yaawn-grill-prototype` | `/yaawn-create-plan` |
43
+ | Other change | matches none of the five types | `Yaawn: Other` | `/yaawn-write-other` | `/yaawn-grill-other` | `/yaawn-create-plan` |
43
44
 
44
45
  ## 1. Greet and choose the type
45
46
 
46
- - **New idea (no `<issue>`):** greet the user and offer the five types through the ask tool, each with its one-line "use when", plus a last option: **"Not sure / none of these"**.
47
+ - **New idea (no `<issue>`):** greet the user and offer the six types through the ask tool, each with its one-line "use when" — for a prototype: "Prototype — build a throwaway version of a feature idea to test with customers and learn what works" — plus a last option: **"Not sure / none of these"**.
47
48
  - **Existing issue:** read it with `gh issue view <issue> --json title,body,labels,comments`. Infer the type from its type label (table above) and ask the user to confirm it.
48
49
  - **Confirmed:** continue with that type.
49
50
  - **Override:** warn that re-typing rewrites the issue's body to the new type's template — the current body's sections are replaced — and ask for explicit confirmation. Only after it, continue with the new type; content worth keeping is carried over only with the user's agreement. Without confirmation, keep the inferred type.
50
51
  - **No type label:** fall back to the picker, as for a new idea.
51
- - **"Not sure / none of these":** help find the right shape — ask what the work changes: behavior users see (story), the inside without user-visible change (refactor), wrong behavior today (bug report), or nothing yet because a question must be answered first (spike). Offer the matching type. If nothing fits, offer **other change** as the last resort; if the user declines it too, say so and stop — never save anything on this path.
52
+ - **"Not sure / none of these":** help find the right shape — ask what the work changes: behavior users see (story), the inside without user-visible change (refactor), wrong behavior today (bug report), nothing yet because a question must be answered first (spike), or a throwaway version of a feature idea to test with customers before committing to it (prototype). Offer the matching type. If nothing fits, offer **other change** as the last resort; if the user declines it too, say so and stop — never save anything on this path.
52
53
 
53
54
  If a writer skill later finds the item is a different type (for example a refactor that changes user-visible behavior), return here and re-offer the type choice.
54
55
 
55
56
  ## 2. Route
56
57
 
57
58
  - **Story:** follow the [Story path](#story-path). For a confirmed re-type, tell `/yaawn-write-user-story` to draft the body from the story template as for a new story, and swap the type label after the save as in [Writer path](#writer-path) step 3.
58
- - **Refactor, spike, bug report, other change:** follow the [Writer path](#writer-path) with the routed writer and grill skills from the table.
59
+ - **Refactor, spike, bug report, prototype, other change:** follow the [Writer path](#writer-path) with the routed writer and grill skills from the table.
59
60
 
60
61
  ## Writer path
61
62
 
62
- 1. **Draft, grill, check:** run step 1 of the routed writer skill (draft or refine), then the routed type's `/yaawn-grill-*` skill from the table — tell it to skip its scope check and that this skill defines what happens when the grill ends: continue with the writer's step 3 (quality check). The grill replaces the writer's step 2 (light grill) in this session only. For an existing issue, the writer reads the issue itself; for a confirmed re-type, tell it to build the body from its own template as for a new issue.
63
- 2. **Approve:** present the finished fields and ask the user to approve them through the ask tool. Wait for it — never create or edit the issue before.
64
- 3. **Save:** run the writer's step 4 ("Saving the issue"). Tell it this approval is the one its saving step 1 asks for — do not ask again. It reads the template frontmatter and saves with `--body-file`. For a confirmed re-type, also swap the old type label for the new one: `gh issue edit <issue> --remove-label "<old type label>" --add-label "<new type label>"`, with both labels taken from the template frontmatters.
63
+ 1. **Draft, grill, check:** run step 1 of the routed writer skill (draft or refine), then the routed type's `/yaawn-grill-*` skill from the table — tell it to skip its scope check and that this skill defines what happens when the grill ends: continue with the writer's step 3 (quality check). The grill replaces the writer's step 2 (light grill) in this session only. When the grill ends, keep the `## Decisions` and `## Open questions` sections from its Result for the save. For an existing issue, the writer reads the issue itself; for a confirmed re-type, tell it to build the body from its own template as for a new issue.
64
+ 2. **Approve:** present the finished fields, together with the grill's decisions and open questions, and ask the user to approve them through the ask tool. Wait for it — never create or edit the issue before.
65
+ 3. **Save:** run the writer's step 4 ("Approve and save"), which follows its "Saving the issue". Tell it this approval is the one its saving step 1 asks for — do not ask again. Pass it the grill's `## Decisions` and `## Open questions` sections as they are, so it fills the issue's `### Design Decisions` and `### Open Questions` tables in the same save — except for a spike, whose template has neither table, so nothing is passed. It reads the template frontmatter and saves with `--body-file`. For a confirmed re-type, also swap the old type label for the new one: `gh issue edit <issue> --remove-label "<old type label>" --add-label "<new type label>"`, with both labels taken from the template frontmatters.
65
66
  4. **Hand off:**
66
- - **Refactor, bug report, other change:** tell the user to start a **new session** and run `/yaawn-create-plan <issue>` there.
67
+ - **Refactor, bug report, prototype, other change:** tell the user to start a **new session** and run `/yaawn-create-plan <issue>` there. For a prototype, add that it is built on a never-merged `prototype/` branch and that `/yaawn-extract-learnings <issue>` replaces `/yaawn-verify-executed-plan` after `/yaawn-implement-plan`.
67
68
  - **Spike:** the session ends here — a human answers the spike. There is no handoff to `/yaawn-create-plan`; tell the user so.
68
69
 
69
70
  ## Story path
@@ -97,7 +98,7 @@ Then, in this session:
97
98
 
98
99
  ### 4. Grill the story
99
100
 
100
- Run `/yaawn-grill-story` on the story. Tell it to skip its scope check — the sprint-fit check or the split assessment already covered scope — and that this skill defines what happens when the grill ends: continue with step 5.
101
+ Run `/yaawn-grill-story` on the story. Tell it to skip its scope check — the sprint-fit check or the split assessment already covered scope — and that this skill defines what happens when the grill ends: continue with step 5. Keep the `## Decisions` and `## Open questions` sections from its Result for step 6.
101
102
 
102
103
  ### 5. Quality check
103
104
 
@@ -105,6 +106,6 @@ Run the quality check from `/yaawn-write-user-story` step 6 on the grilled story
105
106
 
106
107
  ### 6. Approve and save
107
108
 
108
- Ask the user to approve the finished story, then create or update the issue with `/yaawn-write-user-story` step 8. Leave the `Yaawn: To Be Refined` label in place.
109
+ Ask the user to approve the finished story, together with the grill's decisions and open questions, then create or update the issue with `/yaawn-write-user-story` step 8. Pass it the grill's `## Decisions` and `## Open questions` sections as they are, so it fills the issue's `### Design Decisions` and `### Open Questions` tables in the same save. Leave the `Yaawn: To Be Refined` label in place.
109
110
 
110
111
  End by telling the user to start a **new session** and run `/yaawn-create-plan <issue>` there.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yaawn-save-plan
3
- description: "Save the plan /yaawn-create-plan wrote to .yaawn/plans/yaawn-<issue>-plan.md into the GitHub issue's \"Implementation Plan\" section — open questions, design decisions, implementation steps, and an ELI5 Human Readable summary — and remove the Yaawn: To Be Refined label. Does not execute the plan (use yaawn-implement-plan) or verify it (use yaawn-verify-executed-plan)."
3
+ description: "Save the plan /yaawn-create-plan wrote to .yaawn/plans/yaawn-<issue>-plan.md into the GitHub issue's \"Implementation Plan\" section — open questions, design decisions, implementation steps, and an ELI5 Human Readable summary — and remove the Yaawn: To Be Refined label. Offers to answer the plan's open questions with the user before saving. Does not execute the plan (use yaawn-implement-plan) or verify it (use yaawn-verify-executed-plan)."
4
4
  disable-model-invocation: true
5
5
  argument-hint: "[GitHub issue number]"
6
6
  metadata:
@@ -10,7 +10,7 @@ metadata:
10
10
 
11
11
  Start this skill in a new session. It works only from files and the issue, never from an earlier conversation: the plan is the file `/yaawn-create-plan` left at `.yaawn/plans/yaawn-<issue>-plan.md` in one of the project's git worktrees.
12
12
 
13
- Out of scope: changing the plan, grilling it again, and touching the story or acceptance criteria. To change the plan, run `/yaawn-create-plan` again.
13
+ Out of scope: changing the plan beyond answering its open questions, grilling it again, and touching the story or acceptance criteria. To change the plan otherwise, run `/yaawn-create-plan` again.
14
14
 
15
15
  ## 1. Find the plan file
16
16
 
@@ -28,13 +28,32 @@ The plan file may live in another git worktree than the one this session runs in
28
28
 
29
29
  If no issue is given, the script infers it from the `yaawn-<issue>-plan.md` files in `.yaawn/plans/` of the current worktree, every `yaawn/<n>` worktree and the main copy together. Exactly one match: it is used. Several matches: the script lists them — ask which one. No match: ask for the issue number.
30
30
 
31
- ## 2. Write the Human Readable summary
31
+ ## 2. Answer the open questions
32
+
33
+ ```sh
34
+ npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts open-questions <issue>
35
+ ```
36
+
37
+ The script prints the plan's `### Open Questions` rows whose Answer cell is empty — the questions `/yaawn-create-plan` left open, typically facts only the user knows (e.g. whether a feature flag exists in the flag provider).
38
+
39
+ - `open questions: 0`: continue with step 3.
40
+ - Otherwise ask the user through the ask tool (the ask-tool table and the option conventions are in [`.agents/yaawn-grilling.md`](../../yaawn-grilling.md)): the plan has N open questions — answer them now, before the save? Options: **Yes, one by one** and **No, save with them open**. If she declines, continue with step 3; the save still works, the issue just is not marked ready (step 5 reports why).
41
+ - If she answers, go through the questions **one by one** in table order. For each, read the plan file and the issue for context, then ask through the ask tool with 2-4 possible answers, one marked as the recommendation in its description (e.g. "Recommended: ..."), keeping the free-text "Other" option available. Ground the suggestions in the plan's `### Design Decisions` and the issue — never invent facts. When the answer is a fact only the user knows, offer the plausible states (e.g. "Yes — created", "No — not yet", "Not needed") and recommend the one the plan's steps assume: a plan that lists "create the flag" as a manual step assumes it does not exist yet. Wait for each answer before asking the next; if she wants to leave a question open, omit it.
42
+ - Write her answers to `.yaawn/plans/yaawn-<issue>-answers.json` in the current worktree — a JSON object mapping each question's `#` to her answer, e.g. `{ "1": "Yes — the flag exists", "2": "Redis" }`. Then run:
43
+
44
+ ```sh
45
+ npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts answer <issue>
46
+ ```
47
+
48
+ The script fills the Answer cells in the plan file (escaping `|` inside an answer), never overwrites an already-answered row, and deletes the answers file afterwards. An unknown question number means the plan changed since the questions were listed — list them again with `open-questions` and repeat. The answers live in the plan file, so the summary, preview, and save below pick them up.
49
+
50
+ ## 3. Write the Human Readable summary
32
51
 
33
52
  Read the plan file and write a **short** "Human Readable" version of it to the summary path step 1 printed — `.yaawn/plans/yaawn-<issue>-human-readable.md` next to the plan file, in the same worktree — following `.agents/output-styles/yaawn-eli5.instructions.md`. Write only the content — no `### Human Readable` heading; the script adds the content under the existing one. Inside the summary, use only `####` or deeper headings — never `#`, `##`, or `###`. The content lands inside the issue's `## Implementation Plan` section, and a shallower heading would restructure the template and truncate the plan `/yaawn-implement-plan` later extracts. The script demotes any heading that is too shallow, so the issue's heading structure never changes.
34
53
 
35
54
  Explain the **implementation only**. Do not explain the feature; what the user gets is already in the issue's story and acceptance criteria. If applicable, add a mermaid diagram showing the architecture of the plan. Keep the diagram on a conceptual, high level — it is meant to give an overview.
36
55
 
37
- ## 3. Preview
56
+ ## 4. Preview
38
57
 
39
58
  ```sh
40
59
  npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts preview <issue>
@@ -43,10 +62,10 @@ npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts preview <issue>
43
62
  The script ([scripts/save-plan.ts](./scripts/save-plan.ts), with the section logic in [scripts/plan-body.ts](./scripts/plan-body.ts)) fetches the issue body fresh and builds the new body: it replaces the content under `### Human Readable`, `### Open Questions`, `### Design Decisions`, `### Files to Change`, and `### LLM Plan` inside `## Implementation Plan`. Each subsection is replaced as a whole block, so re-saving onto an older issue that still carries a `#### Definition of Done` section under `### LLM Plan` drops that stale copy as a side effect — the Definition of Done lives only in `.yaawn/yaawn-definition-of-done.md`. Headings inside the inserted content are demoted below `###`, so the inserted content can never change the issue's heading structure — and re-saving an issue whose plan section an older yaawn broke with `##`-level content repairs it. Nothing outside that section changes — the story and acceptance criteria are never touched. An issue without `## Implementation Plan` (created without a template) gets the section appended with all five subsections. An older issue whose `## Implementation Plan` lacks `### Files to Change` gets that heading inserted right before `### LLM Plan`. It prints whether the issue already has a plan and the diff of the body.
44
63
 
45
64
  - An error: report it and stop. Any other missing subsection heading means the issue's `## Implementation Plan` is incomplete — the user adds the heading, then runs this skill again.
46
- - `existing plan: no`: continue with step 4.
65
+ - `existing plan: no`: continue with step 5.
47
66
  - `existing plan: yes`: summarize the diff in a few lines — which steps, decisions, and questions change — and ask **one** question through the ask tool: overwrite the plan on the issue? Do not grill or reconsider the plan here; the plan file is the reviewed result of `/yaawn-create-plan`. If the user declines, stop.
48
67
 
49
- ## 4. Save
68
+ ## 5. Save
50
69
 
51
70
  ```sh
52
71
  npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts save <issue>
@@ -60,6 +79,6 @@ The issue body snapshots (`yaawn-<issue>-body-current.md`, `yaawn-<issue>-body.m
60
79
 
61
80
  Leave the working files under `.yaawn/plans/` in place — the developer cleans them up manually.
62
81
 
63
- ## 5. Hand off
82
+ ## 6. Hand off
64
83
 
65
84
  Tell the user: once every issue that belongs in the run has a saved plan, start a new session and run `/yaawn-implement-plan <issue>`.