@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.
Files changed (101) hide show
  1. package/README.md +172 -2
  2. package/agents/instructions/output-styles/yaawn-eli5.instructions.md +45 -0
  3. package/agents/instructions/yaawn-accessibility.css.instructions.md +58 -0
  4. package/agents/instructions/yaawn-accessibility.html.instructions.md +90 -0
  5. package/agents/instructions/yaawn-accessibility.javascript.instructions.md +50 -0
  6. package/agents/instructions/yaawn-clean-code.typescript.instructions.md +74 -0
  7. package/agents/instructions/yaawn-function-coupling.typescript.instructions.md +100 -0
  8. package/agents/instructions/yaawn-html-css-js.html.instructions.md +32 -0
  9. package/agents/instructions/yaawn-open-source-first.instructions.md +38 -0
  10. package/agents/instructions/yaawn-personas.instructions.md +18 -0
  11. package/agents/instructions/yaawn-semantic-html.html.instructions.md +40 -0
  12. package/agents/instructions/yaawn-vanilla-css.css.instructions.md +50 -0
  13. package/agents/output-styles/yaawn-eli5.instructions.md +45 -0
  14. package/agents/personas/yaawn-developer.md +16 -0
  15. package/agents/personas/yaawn-product-owner.md +18 -0
  16. package/agents/personas/yaawn-reviewer.md +16 -0
  17. package/agents/skills/yaawn-clean-code/SKILL.md +61 -0
  18. package/agents/skills/yaawn-clean-code/references/classes.md +114 -0
  19. package/agents/skills/yaawn-clean-code/references/comments.md +164 -0
  20. package/agents/skills/yaawn-clean-code/references/error-handling.md +151 -0
  21. package/agents/skills/yaawn-clean-code/references/formatting.md +146 -0
  22. package/agents/skills/yaawn-clean-code/references/functions.md +261 -0
  23. package/agents/skills/yaawn-clean-code/references/meaningful-names.md +234 -0
  24. package/agents/skills/yaawn-clean-code/references/objects-and-data-structures.md +130 -0
  25. package/agents/skills/yaawn-clean-code/references/principles.md +76 -0
  26. package/agents/skills/yaawn-clean-code/references/simple-design-and-code-smells.md +130 -0
  27. package/agents/skills/yaawn-clean-code/references/unit-tests.md +114 -0
  28. package/agents/skills/yaawn-create-plan/SKILL.md +228 -0
  29. package/agents/skills/yaawn-grill-bug-report/SKILL.md +31 -0
  30. package/agents/skills/yaawn-grill-bug-report/references/bug-report-checklist.md +27 -0
  31. package/agents/skills/yaawn-grill-design/SKILL.md +68 -0
  32. package/agents/skills/yaawn-grill-design/references/design-checklist.md +36 -0
  33. package/agents/skills/yaawn-grill-other/SKILL.md +31 -0
  34. package/agents/skills/yaawn-grill-other/references/other-checklist.md +19 -0
  35. package/agents/skills/yaawn-grill-refactor/SKILL.md +31 -0
  36. package/agents/skills/yaawn-grill-refactor/references/refactor-checklist.md +26 -0
  37. package/agents/skills/yaawn-grill-spike/SKILL.md +31 -0
  38. package/agents/skills/yaawn-grill-spike/references/spike-checklist.md +25 -0
  39. package/agents/skills/yaawn-grill-story/SKILL.md +34 -0
  40. package/agents/skills/yaawn-grill-story/references/concept-challenge.md +67 -0
  41. package/agents/skills/yaawn-grill-story/references/story-checklist.md +43 -0
  42. package/agents/skills/yaawn-implement-plan/SKILL.md +196 -0
  43. package/agents/skills/yaawn-implement-plan/assets/fix-prompt.md +66 -0
  44. package/agents/skills/yaawn-implement-plan/assets/review-prompt.md +89 -0
  45. package/agents/skills/yaawn-implement-plan/assets/session-prompt.md +80 -0
  46. package/agents/skills/yaawn-implement-plan/scripts/agent.ts +218 -0
  47. package/agents/skills/yaawn-implement-plan/scripts/caffeinate.ts +56 -0
  48. package/agents/skills/yaawn-implement-plan/scripts/groups.ts +233 -0
  49. package/agents/skills/yaawn-implement-plan/scripts/overnight.ts +772 -0
  50. package/agents/skills/yaawn-implement-plan/scripts/parallel.ts +1176 -0
  51. package/agents/skills/yaawn-implement-plan/scripts/processes.ts +53 -0
  52. package/agents/skills/yaawn-implement-plan/scripts/stages.ts +65 -0
  53. package/agents/skills/yaawn-implement-plan/scripts/targets.ts +381 -0
  54. package/agents/skills/yaawn-implement-plan/scripts/worktrees.ts +431 -0
  55. package/agents/skills/yaawn-owasp-security/SKILL.md +106 -0
  56. package/agents/skills/yaawn-owasp-security/references/a01-broken-access-control.md +58 -0
  57. package/agents/skills/yaawn-owasp-security/references/a02-security-misconfiguration.md +56 -0
  58. package/agents/skills/yaawn-owasp-security/references/a03-software-supply-chain-failures.md +56 -0
  59. package/agents/skills/yaawn-owasp-security/references/a04-cryptographic-failures.md +67 -0
  60. package/agents/skills/yaawn-owasp-security/references/a05-injection.md +53 -0
  61. package/agents/skills/yaawn-owasp-security/references/a06-insecure-design.md +48 -0
  62. package/agents/skills/yaawn-owasp-security/references/a07-authentication-failures.md +68 -0
  63. package/agents/skills/yaawn-owasp-security/references/a08-software-data-integrity-failures.md +45 -0
  64. package/agents/skills/yaawn-owasp-security/references/a09-security-logging-alerting-failures.md +59 -0
  65. package/agents/skills/yaawn-owasp-security/references/a10-mishandling-exceptional-conditions.md +63 -0
  66. package/agents/skills/yaawn-prepare-plan/SKILL.md +110 -0
  67. package/agents/skills/yaawn-save-plan/SKILL.md +65 -0
  68. package/agents/skills/yaawn-save-plan/scripts/plan-body.ts +331 -0
  69. package/agents/skills/yaawn-save-plan/scripts/save-plan.ts +443 -0
  70. package/agents/skills/yaawn-split-story/SKILL.md +189 -0
  71. package/agents/skills/yaawn-split-story/references/spider.md +64 -0
  72. package/agents/skills/yaawn-split-story/scripts/create-issue.ts +162 -0
  73. package/agents/skills/yaawn-verify-executed-plan/SKILL.md +154 -0
  74. package/agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts +362 -0
  75. package/agents/skills/yaawn-verify-executed-plan/scripts/pull-request.ts +131 -0
  76. package/agents/skills/yaawn-write-bug-report/SKILL.md +87 -0
  77. package/agents/skills/yaawn-write-other/SKILL.md +90 -0
  78. package/agents/skills/yaawn-write-refactor/SKILL.md +94 -0
  79. package/agents/skills/yaawn-write-spike/SKILL.md +94 -0
  80. package/agents/skills/yaawn-write-user-story/SKILL.md +187 -0
  81. package/agents/skills/yaawn-write-user-story/references/acceptance-criteria.md +186 -0
  82. package/agents/skills/yaawn-write-user-story/references/five-types-of-value.md +82 -0
  83. package/agents/skills/yaawn-write-user-story/references/user-story-fundamentals.md +75 -0
  84. package/agents/yaawn-glossary.md +146 -0
  85. package/agents/yaawn-grilling.md +75 -0
  86. package/agents/yaawn-worktrees.md +69 -0
  87. package/config/install-tasks.json +35 -0
  88. package/config/settings.json +23 -0
  89. package/config/wizard.json +128 -0
  90. package/github/ISSUE_TEMPLATE/yaawn-bug-report.md +59 -0
  91. package/github/ISSUE_TEMPLATE/yaawn-other.md +38 -0
  92. package/github/ISSUE_TEMPLATE/yaawn-refactor.md +38 -0
  93. package/github/ISSUE_TEMPLATE/yaawn-spike.md +16 -0
  94. package/github/ISSUE_TEMPLATE/yaawn-story.md +56 -0
  95. package/package.json +33 -4
  96. package/scripts/install.js +73 -0
  97. package/yaawn/gitignore +1 -0
  98. package/yaawn/yaawn-definition-of-done.md +5 -0
  99. package/yaawn/yaawn-project-glossary.md +7 -0
  100. package/yaawn/yaawn-user-roles.md +3 -0
  101. package/yaawn/yaawn-verification-commands.md +7 -0
@@ -0,0 +1,65 @@
1
+ ---
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)."
4
+ disable-model-invocation: true
5
+ argument-hint: "[GitHub issue number]"
6
+ metadata:
7
+ github-repo: https://github.com/markusfalk/yaawn
8
+ author: markusfalk
9
+ ---
10
+
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
+
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.
14
+
15
+ ## 1. Find the plan file
16
+
17
+ ```sh
18
+ npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts find [issue]
19
+ ```
20
+
21
+ The plan file may live in another git worktree than the one this session runs in, so the script searches the worktrees in order: the current worktree, the issue's own worktree (the one on branch `yaawn/<issue>`), then the main copy. It prints the issue, the plan file, and the summary path.
22
+
23
+ - The current worktree holds the plan: it wins.
24
+ - Exactly one other worktree holds it: that plan is used.
25
+ - Several worktrees hold it and the session runs in none of them: the script stops and names their paths — tell the user to run this skill in the one whose plan they want to save. It never picks one silently.
26
+ - No worktree holds it: stop and tell the user to start a new session and run `/yaawn-create-plan <issue>` first. Never rebuild a plan from a conversation.
27
+ - `git worktree list` fails (no git, no repository): only the current directory is searched.
28
+
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
+
31
+ ## 2. Write the Human Readable summary
32
+
33
+ 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
+
35
+ 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
+
37
+ ## 3. Preview
38
+
39
+ ```sh
40
+ npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts preview <issue>
41
+ ```
42
+
43
+ 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
+
45
+ - 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.
47
+ - `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
+
49
+ ## 4. Save
50
+
51
+ ```sh
52
+ npx tsx .agents/skills/yaawn-save-plan/scripts/save-plan.ts save <issue>
53
+ ```
54
+
55
+ It writes the body with `gh issue edit --body-file`, then removes the `Yaawn: To Be Refined` label if present — the saved plan is the refinement. When the save changes `### Files to Change` or the `**Depends on:**` line — both feed the implementation grouping — it also removes the issue's `Yaawn: implementation-group-<n>` labels through `/yaawn-implement-plan`'s shared group-label reset; the implementation group no longer holds. If that removal fails, the script prints a warning and the save still succeeds — the next `/yaawn-implement-plan` run resets the labels when it starts.
56
+
57
+ It then adds the `Yaawn: Ready To Implement` label (creating the label in the repository if missing) when the plan's `### Open Questions` table has no question with an empty Answer and `### Files to Change` lists at least one file. Otherwise it prints why the issue was not marked ready — report that to the user. If adding the label fails, the saved body stays and the script prints a warning; tell the user to add the label manually. Edit the issue body only — never create comments on the issue.
58
+
59
+ The issue body snapshots (`yaawn-<issue>-body-current.md`, `yaawn-<issue>-body.md`) always go to the current worktree's `.yaawn/plans/`, wherever the plan file lives.
60
+
61
+ Leave the working files under `.yaawn/plans/` in place — the developer cleans them up manually.
62
+
63
+ ## 5. Hand off
64
+
65
+ 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>`.
@@ -0,0 +1,331 @@
1
+ // Pure markdown-section machinery for yaawn-save-plan: finds the
2
+ // "Implementation Plan" section of an issue body and replaces its
3
+ // subsections. No I/O — save-plan.ts reads files and calls gh, this module
4
+ // only computes.
5
+
6
+ export interface Heading {
7
+ level: number;
8
+ title: string;
9
+ line: number;
10
+ }
11
+
12
+ export interface LineRange {
13
+ start: number;
14
+ end: number;
15
+ }
16
+
17
+ export interface Section {
18
+ headingLine: number;
19
+ contentStart: number;
20
+ contentEnd: number;
21
+ }
22
+
23
+ export const implementationPlanTitle = "Implementation Plan";
24
+ export const humanReadableTitle = "Human Readable";
25
+ export const openQuestionsTitle = "Open Questions";
26
+ export const filesToChangeTitle = "Files to Change";
27
+ export const llmPlanTitle = "LLM Plan";
28
+ export const planSubsections = [
29
+ openQuestionsTitle,
30
+ "Design Decisions",
31
+ filesToChangeTitle,
32
+ llmPlanTitle,
33
+ ];
34
+ export const issueSubsections = [humanReadableTitle, ...planSubsections];
35
+
36
+ const subsectionLevel = 3;
37
+
38
+ export const headingList = (titles: string[]): string =>
39
+ titles.map((title) => `### ${title}`).join(", ");
40
+
41
+ export function toLines(markdown: string): string[] {
42
+ return markdown.split(/\r?\n/);
43
+ }
44
+
45
+ function fenceOpening(line: string): string | null {
46
+ return /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1] ?? null;
47
+ }
48
+
49
+ function closesFence(line: string, fence: string): boolean {
50
+ const closing = /^ {0,3}(`{3,}|~{3,})\s*$/.exec(line)?.[1];
51
+ return (
52
+ closing !== undefined &&
53
+ closing[0] === fence[0] &&
54
+ closing.length >= fence.length
55
+ );
56
+ }
57
+
58
+ // Headings inside fenced code blocks (e.g. a "# comment" in a shell snippet)
59
+ // are not section boundaries.
60
+ export function headingsOf(lines: string[]): Heading[] {
61
+ const headings: Heading[] = [];
62
+ let fence: string | null = null;
63
+ lines.forEach((line, index) => {
64
+ if (fence !== null) {
65
+ if (closesFence(line, fence)) fence = null;
66
+ return;
67
+ }
68
+ fence = fenceOpening(line);
69
+ if (fence !== null) return;
70
+ const match = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
71
+ if (match) {
72
+ headings.push({ level: match[1].length, title: match[2], line: index });
73
+ }
74
+ });
75
+ return headings;
76
+ }
77
+
78
+ export function findSection(
79
+ lines: string[],
80
+ level: number,
81
+ title: string,
82
+ within: LineRange = { start: 0, end: lines.length },
83
+ ): Section | null {
84
+ const inRange = headingsOf(lines).filter(
85
+ (heading) => heading.line >= within.start && heading.line < within.end,
86
+ );
87
+ const heading = inRange.find(
88
+ (candidate) => candidate.level === level && candidate.title === title,
89
+ );
90
+ if (!heading) return null;
91
+ const next = inRange.find(
92
+ (candidate) => candidate.line > heading.line && candidate.level <= level,
93
+ );
94
+ return {
95
+ headingLine: heading.line,
96
+ contentStart: heading.line + 1,
97
+ contentEnd: next?.line ?? within.end,
98
+ };
99
+ }
100
+
101
+ function contentRange(section: Section): LineRange {
102
+ return { start: section.contentStart, end: section.contentEnd };
103
+ }
104
+
105
+ // Content inserted under a ### subsection may only carry headings deeper than
106
+ // the subsection: a heading at level 3 or shallower would end the subsection
107
+ // — or the whole ## Implementation Plan section — and restructure the issue
108
+ // template, so /yaawn-implement-plan would later extract a truncated plan.
109
+ // Shift every heading deeper so the shallowest one lands at minLevel, keeping
110
+ // the content's own hierarchy.
111
+ export function demoteHeadings(markdown: string, minLevel: number): string {
112
+ const lines = toLines(markdown);
113
+ const headings = headingsOf(lines);
114
+ if (headings.length === 0) return markdown;
115
+ const shallowest = Math.min(...headings.map((heading) => heading.level));
116
+ const shift = minLevel - shallowest;
117
+ if (shift <= 0) return markdown;
118
+ headings.forEach((heading) => {
119
+ const level = Math.min(6, heading.level + shift);
120
+ lines[heading.line] =
121
+ "#".repeat(level) + lines[heading.line].slice(heading.level);
122
+ });
123
+ return lines.join("\n");
124
+ }
125
+
126
+ function fitUnderSubsections(content: Map<string, string>): Map<string, string> {
127
+ return new Map(
128
+ [...content].map(([title, value]) => [
129
+ title,
130
+ demoteHeadings(value, subsectionLevel + 1),
131
+ ]),
132
+ );
133
+ }
134
+
135
+ // An issue saved by an older yaawn could carry shallow content headings (##
136
+ // or ###) inside the plan, which end the bounded plan range early and hide
137
+ // the later subsections. When that is the case, demote every stray heading
138
+ // below the subsection level — the subsection replacement rewrites their
139
+ // content anyway — so the subsections are found and a re-save restores the
140
+ // template structure. A healthy body is returned unchanged.
141
+ function healedPlanLines(lines: string[], plan: Section): string[] {
142
+ const bounded = contentRange(plan);
143
+ const complete = issueSubsections.every(
144
+ (title) => findSection(lines, 3, title, bounded) !== null,
145
+ );
146
+ if (complete) return lines;
147
+ const healed = [...lines];
148
+ headingsOf(lines).forEach((heading) => {
149
+ if (heading.line < plan.contentStart) return;
150
+ if (heading.level === 3 && issueSubsections.includes(heading.title)) return;
151
+ if (heading.level > 3) return;
152
+ healed[heading.line] = "####" + healed[heading.line].slice(heading.level);
153
+ });
154
+ return healed;
155
+ }
156
+
157
+ function appendImplementationPlan(
158
+ body: string,
159
+ content: Map<string, string>,
160
+ ): string {
161
+ const subsections = issueSubsections.map(
162
+ (title) => `### ${title}\n\n${content.get(title)}`,
163
+ );
164
+ return [body, `## ${implementationPlanTitle}`, ...subsections].join("\n\n");
165
+ }
166
+
167
+ function findIssueSubsections(
168
+ lines: string[],
169
+ within: LineRange,
170
+ ): { title: string; section: Section }[] {
171
+ const found = issueSubsections.map((title) => ({
172
+ title,
173
+ section: findSection(lines, 3, title, within),
174
+ }));
175
+ const missing = found.filter(({ section }) => section === null);
176
+ if (missing.length > 0) {
177
+ throw new Error(
178
+ `the issue's "## ${implementationPlanTitle}" lacks: ${headingList(missing.map(({ title }) => title))} — add the heading to the issue, then run again`,
179
+ );
180
+ }
181
+ return found as { title: string; section: Section }[];
182
+ }
183
+
184
+ function replaceSubsections(
185
+ lines: string[],
186
+ within: LineRange,
187
+ content: Map<string, string>,
188
+ ): string[] {
189
+ const bottomUp = findIssueSubsections(lines, within).sort(
190
+ (a, b) => b.section.headingLine - a.section.headingLine,
191
+ );
192
+ return bottomUp.reduce((result, { title, section }) => {
193
+ const replacement = ["", ...toLines(content.get(title) ?? ""), ""];
194
+ return [
195
+ ...result.slice(0, section.contentStart),
196
+ ...replacement,
197
+ ...result.slice(section.contentEnd),
198
+ ];
199
+ }, lines);
200
+ }
201
+
202
+ // Issues created before "### Files to Change" existed get the heading
203
+ // inserted right before "### LLM Plan".
204
+ function withFilesToChangeHeading(
205
+ lines: string[],
206
+ within: LineRange,
207
+ ): string[] {
208
+ if (findSection(lines, 3, filesToChangeTitle, within)) return lines;
209
+ const llmPlan = findSection(lines, 3, llmPlanTitle, within);
210
+ if (!llmPlan) return lines;
211
+ return [
212
+ ...lines.slice(0, llmPlan.headingLine),
213
+ `### ${filesToChangeTitle}`,
214
+ "",
215
+ ...lines.slice(llmPlan.headingLine),
216
+ ];
217
+ }
218
+
219
+ export function buildBody(body: string, content: Map<string, string>): string {
220
+ const safeContent = fitUnderSubsections(content);
221
+ const originalLines = toLines(body);
222
+ const originalPlan = findSection(originalLines, 2, implementationPlanTitle);
223
+ if (!originalPlan) {
224
+ return `${appendImplementationPlan(body, safeContent).trimEnd()}\n`;
225
+ }
226
+ const healed = healedPlanLines(originalLines, originalPlan);
227
+ const healedPlan = findSection(
228
+ healed,
229
+ 2,
230
+ implementationPlanTitle,
231
+ ) as Section;
232
+ const lines = withFilesToChangeHeading(healed, contentRange(healedPlan));
233
+ const plan = findSection(lines, 2, implementationPlanTitle) as Section;
234
+ const newBody = replaceSubsections(
235
+ lines,
236
+ contentRange(plan),
237
+ safeContent,
238
+ ).join("\n");
239
+ return `${newBody.trimEnd()}\n`;
240
+ }
241
+
242
+ export function ownLlmPlanContent(body: string): string {
243
+ const lines = toLines(body);
244
+ const plan = findSection(lines, 2, implementationPlanTitle);
245
+ if (!plan) return "";
246
+ const healed = healedPlanLines(lines, plan);
247
+ const healedPlan = findSection(healed, 2, implementationPlanTitle);
248
+ if (!healedPlan) return "";
249
+ const llmPlan = findSection(healed, 3, llmPlanTitle, contentRange(healedPlan));
250
+ if (!llmPlan) return "";
251
+ const firstSubheading = headingsOf(healed).find(
252
+ (heading) =>
253
+ heading.line > llmPlan.headingLine && heading.line < llmPlan.contentEnd,
254
+ );
255
+ return healed
256
+ .slice(llmPlan.contentStart, firstSubheading?.line ?? llmPlan.contentEnd)
257
+ .join("\n")
258
+ .replace(/<!--[\s\S]*?-->/g, "")
259
+ .trim();
260
+ }
261
+
262
+ export function filesToChangeContent(body: string): string {
263
+ const lines = toLines(body);
264
+ const plan = findSection(lines, 2, implementationPlanTitle);
265
+ if (!plan) return "";
266
+ const healed = healedPlanLines(lines, plan);
267
+ const healedPlan = findSection(healed, 2, implementationPlanTitle);
268
+ if (!healedPlan) return "";
269
+ const filesToChange = findSection(
270
+ healed,
271
+ 3,
272
+ filesToChangeTitle,
273
+ contentRange(healedPlan),
274
+ );
275
+ if (!filesToChange) return "";
276
+ return healed
277
+ .slice(filesToChange.contentStart, filesToChange.contentEnd)
278
+ .join("\n")
279
+ .trim();
280
+ }
281
+
282
+ const withoutHtmlComments = (markdown: string): string =>
283
+ markdown.replace(/<!--[\s\S]*?-->/g, "");
284
+
285
+ function tableCells(row: string): string[] {
286
+ return row
287
+ .trim()
288
+ .replace(/^\|/, "")
289
+ .replace(/\|$/, "")
290
+ .split(/(?<!\\)\|/)
291
+ .map((cell) => cell.trim());
292
+ }
293
+
294
+ function columnIndex(header: string[], name: string, fallback: number): number {
295
+ const index = header.indexOf(name);
296
+ return index < 0 ? fallback : index;
297
+ }
298
+
299
+ function countUnansweredQuestions(openQuestions: string): number {
300
+ const rows = toLines(withoutHtmlComments(openQuestions)).filter((line) =>
301
+ line.trim().startsWith("|"),
302
+ );
303
+ const [header, ...body] = rows.map(tableCells);
304
+ if (!header) return 0;
305
+ // Fall back to the template's column layout so an unknown header fails closed.
306
+ const questionColumn = columnIndex(header, "Questions", 1);
307
+ const answerColumn = columnIndex(header, "Answer", 3);
308
+ return body.filter(
309
+ (cells) =>
310
+ !cells.every((cell) => /^:?-+:?$/.test(cell)) &&
311
+ (cells[questionColumn] ?? "") !== "" &&
312
+ (cells[answerColumn] ?? "") === "",
313
+ ).length;
314
+ }
315
+
316
+ function hasFilesToChange(filesToChange: string): boolean {
317
+ return toLines(withoutHtmlComments(filesToChange)).some((line) =>
318
+ /^\s*[-*+]\s+\S/.test(line),
319
+ );
320
+ }
321
+
322
+ export function readyBlocker(content: Map<string, string>): string | null {
323
+ const unanswered = countUnansweredQuestions(
324
+ content.get(openQuestionsTitle) ?? "",
325
+ );
326
+ if (unanswered > 0) return `${unanswered} unanswered question(s)`;
327
+ if (!hasFilesToChange(content.get(filesToChangeTitle) ?? "")) {
328
+ return `### ${filesToChangeTitle} is empty`;
329
+ }
330
+ return null;
331
+ }