@yaawn/workflow 0.0.0-stage → 0.4.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 (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 +234 -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 +77 -0
  41. package/agents/skills/yaawn-grill-story/references/story-checklist.md +44 -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 +84 -0
  68. package/agents/skills/yaawn-save-plan/scripts/plan-body.ts +397 -0
  69. package/agents/skills/yaawn-save-plan/scripts/save-plan.ts +539 -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,53 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import type { Readable } from "node:stream";
3
+ import { finished } from "node:stream/promises";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+
6
+ const outputDrainTimeoutMs = 5000;
7
+
8
+ function processStreams(child: ChildProcess): Readable[] {
9
+ return [child.stdout, child.stderr].filter(
10
+ (stream): stream is Readable => stream !== null,
11
+ );
12
+ }
13
+
14
+ function ignoreAbort(error: unknown): void {
15
+ if (error instanceof Error && error.name === "AbortError") return;
16
+ throw error;
17
+ }
18
+
19
+ // 'exit' can fire before the last output lines are read. Wait for the pipes
20
+ // to close, but not forever: a background process the child left running may
21
+ // hold them open, which would also keep this orchestrator from exiting.
22
+ // The losing timeout is aborted so no timer outlives the drain.
23
+ export async function drainOutput(
24
+ child: ChildProcess,
25
+ timeoutMs = outputDrainTimeoutMs,
26
+ ): Promise<void> {
27
+ const streams = processStreams(child);
28
+ const timeout = new AbortController();
29
+ const allClosed = Promise.all(
30
+ streams.map((stream) => finished(stream).catch(() => undefined)),
31
+ );
32
+ const timedOut = delay(timeoutMs, undefined, { signal: timeout.signal })
33
+ .catch(ignoreAbort);
34
+ try {
35
+ await Promise.race([allClosed, timedOut]);
36
+ } finally {
37
+ timeout.abort();
38
+ }
39
+ streams.forEach((stream) => stream.destroy());
40
+ }
41
+
42
+ // -pid reaches the child's whole process group; spawn the child detached.
43
+ export function killProcessGroup(
44
+ child: ChildProcess,
45
+ signal: NodeJS.Signals,
46
+ ): void {
47
+ if (child.pid === undefined) return;
48
+ try {
49
+ process.kill(-child.pid, signal);
50
+ } catch (error) {
51
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
52
+ }
53
+ }
@@ -0,0 +1,65 @@
1
+ import { issueRefs } from "./targets.js";
2
+
3
+ export const STAGES = ["implement", "review", "fix"] as const;
4
+
5
+ export type Stage = (typeof STAGES)[number];
6
+
7
+ // A lane that stops before its first stage marker arrives has no stage yet.
8
+ export const laneStartedLabel = "started";
9
+
10
+ export type StageLabel = Stage | typeof laneStartedLabel;
11
+
12
+ export type LaneOutcomeState = "started" | "done" | "failed" | "stopped";
13
+
14
+ export interface LaneOutcome {
15
+ issue: number;
16
+ state: LaneOutcomeState;
17
+ stage?: StageLabel;
18
+ }
19
+
20
+ const stageMarkerPrefix = "yaawn-stage:";
21
+
22
+ export function stageMarker(issue: number, stage: Stage): string {
23
+ return `${stageMarkerPrefix} ${issue} ${stage}`;
24
+ }
25
+
26
+ // Lane output is untrusted: accept only the exact marker of the lane's own issue.
27
+ export function parseStageMarker(line: string, issue: number): Stage | undefined {
28
+ return STAGES.find((stage) => line === stageMarker(issue, stage));
29
+ }
30
+
31
+ export function runningResult(issue: number, stage: StageLabel): string {
32
+ return `#${issue} running — stage ${stage}`;
33
+ }
34
+
35
+ export function stoppedResult(issue: number, stage: StageLabel): string {
36
+ return `#${issue} stopped in ${stage}`;
37
+ }
38
+
39
+ function issuesIn(lanes: LaneOutcome[], states: LaneOutcomeState[]): number[] {
40
+ return lanes
41
+ .filter((lane) => states.includes(lane.state))
42
+ .map((lane) => lane.issue);
43
+ }
44
+
45
+ function failedPart(failed: number[]): string {
46
+ const refs = failed.length > 0 ? ` (${issueRefs(failed)})` : "";
47
+ return `${failed.length} failed${refs}`;
48
+ }
49
+
50
+ function stoppedPart(stopped: LaneOutcome[]): string {
51
+ const refs = stopped
52
+ .map((lane) => `#${lane.issue} in ${lane.stage ?? laneStartedLabel}`)
53
+ .join(", ");
54
+ return `, ${stopped.length} stopped (${refs})`;
55
+ }
56
+
57
+ export function runResultLine(lanes: LaneOutcome[]): string {
58
+ const done = issuesIn(lanes, ["done"]);
59
+ if (done.length === lanes.length)
60
+ return `run result: all ${lanes.length} lanes done`;
61
+ const failed = issuesIn(lanes, ["started", "failed"]);
62
+ const stopped = lanes.filter((lane) => lane.state === "stopped");
63
+ const stoppedSuffix = stopped.length > 0 ? stoppedPart(stopped) : "";
64
+ return `run result: ${done.length} done, ${failedPart(failed)}${stoppedSuffix}`;
65
+ }
@@ -0,0 +1,381 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ import { groupLabelRemovals } from "./groups.js";
5
+
6
+ // Shared by the overnight scripts of yaawn-implement-plan and
7
+ // yaawn-verify-executed-plan: both work on the same targets.
8
+
9
+ export interface Issue {
10
+ number: number;
11
+ title: string;
12
+ body: string;
13
+ dependsOn: number[];
14
+ externalDependsOn: number[];
15
+ }
16
+
17
+ interface RawIssue {
18
+ number: number;
19
+ title: string;
20
+ body: string | null;
21
+ }
22
+
23
+ export const plansDir = ".yaawn/plans";
24
+ const groupLabelColor = "5319E7";
25
+ const groupLabelDescription = "Issues with this label can be implemented together in parallel";
26
+ const execFileAsync = promisify(execFile);
27
+
28
+ export const resultPath = (issue: number): string =>
29
+ `${plansDir}/yaawn-${issue}-overnight-result.md`;
30
+
31
+ export const progressPath = (issue: number): string =>
32
+ `${plansDir}/yaawn-${issue}-progress.md`;
33
+
34
+ export const issueRefs = (issues: (number | string)[], prefix = "#"): string =>
35
+ issues.map((issue) => `${prefix}${issue}`).join(", ");
36
+
37
+ export function gh(args: string[]): string {
38
+ return execFileSync("gh", args, { encoding: "utf-8" });
39
+ }
40
+
41
+ export function succeeds(command: string, args: string[]): boolean {
42
+ try {
43
+ execFileSync(command, args, { stdio: "ignore" });
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ export async function ghAsync(args: string[]): Promise<string> {
51
+ const { stdout } = await execFileAsync("gh", args, { encoding: "utf-8" });
52
+ return stdout;
53
+ }
54
+
55
+ export async function succeedsAsync(command: string, args: string[]): Promise<boolean> {
56
+ try {
57
+ await execFileAsync(command, args);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // --- labels
65
+
66
+ const labelNames = (output: string): string[] =>
67
+ output.split("\n").map((name) => name.trim()).filter(Boolean);
68
+
69
+ export function issueLabels(issue: number): string[] {
70
+ return labelNames(
71
+ gh(["issue", "view", String(issue), "--json", "labels", "--jq", ".labels[].name"]),
72
+ );
73
+ }
74
+
75
+ export function addLabel(issue: number, label: string): void {
76
+ gh(["issue", "edit", String(issue), "--add-label", label]);
77
+ }
78
+
79
+ export function removeLabel(issue: number, label: string): void {
80
+ gh(["issue", "edit", String(issue), "--remove-label", label]);
81
+ }
82
+
83
+ function labelExists(label: string): boolean {
84
+ return labelNames(
85
+ gh(["label", "list", "--search", label, "--limit", "100", "--json", "name", "--jq", ".[].name"]),
86
+ ).includes(label);
87
+ }
88
+
89
+ export function ensureGroupLabel(label: string): void {
90
+ if (labelExists(label)) return;
91
+ try {
92
+ gh(["label", "create", label, "--color", groupLabelColor, "--description", groupLabelDescription]);
93
+ console.log(`created label "${label}"`);
94
+ } catch (error) {
95
+ // Another run may have created it in the meantime ("already exists").
96
+ if (!labelExists(label)) throw error;
97
+ }
98
+ }
99
+
100
+ function errorMessage(error: unknown): string {
101
+ return error instanceof Error ? error.message : String(error);
102
+ }
103
+
104
+ const firstLine = (text: string): string => text.split("\n")[0];
105
+
106
+ // The one reset of the implementation-group labels, shared by every skill that
107
+ // changes a plan. It only warns: a failed reset never blocks the caller.
108
+ export function removeGroupLabels(issue: number): void {
109
+ if (!Number.isSafeInteger(issue) || issue < 1) {
110
+ console.log(`WARN group labels not reset: invalid issue ${JSON.stringify(issue)}`);
111
+ return;
112
+ }
113
+ try {
114
+ groupLabelRemovals(issue, issueLabels(issue)).forEach(({ label }) => {
115
+ removeLabel(issue, label);
116
+ console.log(`removed label "${label}" on #${issue}`);
117
+ });
118
+ } catch (error) {
119
+ console.log(`WARN could not reset group labels on #${issue}: ${firstLine(errorMessage(error))}`);
120
+ }
121
+ }
122
+
123
+ // --- issues
124
+
125
+ const dependsOnPattern = /\*\*Depends on:\*\*\s*(.+)/;
126
+
127
+ export function dependsOnLine(body: string): string {
128
+ return dependsOnPattern.exec(body)?.[0] ?? "";
129
+ }
130
+
131
+ function parseDependsOn(body: string): number[] {
132
+ const line = dependsOnPattern.exec(body)?.[1] ?? "";
133
+ return [...line.matchAll(/#(\d+)/g)].map((match) => Number(match[1]));
134
+ }
135
+
136
+ function toIssue({ number, title, body }: RawIssue): Issue {
137
+ return {
138
+ number,
139
+ title,
140
+ body: body ?? "",
141
+ dependsOn: parseDependsOn(body ?? ""),
142
+ externalDependsOn: [],
143
+ };
144
+ }
145
+
146
+ const fetchIssueArgs = (issue: number): string[] => [
147
+ "issue",
148
+ "view",
149
+ String(issue),
150
+ "--json",
151
+ "number,title,body",
152
+ ];
153
+
154
+ const parseIssue = (output: string): Issue => toIssue(JSON.parse(output) as RawIssue);
155
+
156
+ export function fetchIssue(issue: number): Issue {
157
+ return parseIssue(gh(fetchIssueArgs(issue)));
158
+ }
159
+
160
+ export async function fetchIssueAsync(issue: number): Promise<Issue> {
161
+ return parseIssue(await ghAsync(fetchIssueArgs(issue)));
162
+ }
163
+
164
+ function splitDependencies(targets: Issue[]): Issue[] {
165
+ const targetNumbers = new Set(targets.map((issue) => issue.number));
166
+ return targets.map((issue) => ({
167
+ ...issue,
168
+ dependsOn: issue.dependsOn.filter((dep) => targetNumbers.has(dep)),
169
+ externalDependsOn: issue.dependsOn.filter((dep) => !targetNumbers.has(dep)),
170
+ }));
171
+ }
172
+
173
+ export function resolveTargets(parent: number): Issue[] {
174
+ return splitDependencies([fetchIssue(parent)]);
175
+ }
176
+
177
+ export async function resolveTargetsAsync(parent: number): Promise<Issue[]> {
178
+ return splitDependencies([await fetchIssueAsync(parent)]);
179
+ }
180
+
181
+ const issueStateArgs = (issue: number): string[] => [
182
+ "api",
183
+ `repos/{owner}/{repo}/issues/${issue}`,
184
+ "--jq",
185
+ ".state",
186
+ ];
187
+
188
+ export function isOpen(issue: number): boolean {
189
+ return gh(issueStateArgs(issue)).trim() === "open";
190
+ }
191
+
192
+ export async function isOpenAsync(issue: number): Promise<boolean> {
193
+ return (await ghAsync(issueStateArgs(issue))).trim() === "open";
194
+ }
195
+
196
+ export function isDone(issue: number): boolean {
197
+ if (!existsSync(progressPath(issue))) return false;
198
+ const progress = readFileSync(progressPath(issue), "utf-8");
199
+ return progress.includes("- [x]") && !progress.includes("- [ ]");
200
+ }
201
+
202
+ // Dependencies first; issues whose dependencies are equally settled keep
203
+ // ascending issue-number order.
204
+ export function executionOrder(issues: Issue[]): Issue[] {
205
+ const placed = new Set<number>();
206
+ const ordered: Issue[] = [];
207
+ let remaining = [...issues].sort((a, b) => a.number - b.number);
208
+ while (remaining.length > 0) {
209
+ const ready = remaining.filter((issue) =>
210
+ issue.dependsOn.every((dep) => placed.has(dep)),
211
+ );
212
+ if (ready.length === 0) {
213
+ throw new Error(
214
+ `Circular dependency among ${issueRefs(remaining.map((issue) => issue.number))}`,
215
+ );
216
+ }
217
+ ready.forEach((issue) => placed.add(issue.number));
218
+ ordered.push(...ready);
219
+ remaining = remaining.filter((issue) => !placed.has(issue.number));
220
+ }
221
+ return ordered;
222
+ }
223
+
224
+ // The planning and run files under .yaawn/plans/ are not the human's changes.
225
+ const uncommittedChangesArgs = ["status", "--short", "--", ".", `:!${plansDir}`];
226
+
227
+ export function uncommittedChanges(): string {
228
+ return execFileSync("git", uncommittedChangesArgs, { encoding: "utf-8" });
229
+ }
230
+
231
+ export async function uncommittedChangesAsync(): Promise<string> {
232
+ const { stdout } = await execFileAsync("git", uncommittedChangesArgs, {
233
+ encoding: "utf-8",
234
+ });
235
+ return stdout;
236
+ }
237
+
238
+ const fenceMarkers = ["```", "~~~"];
239
+ const sectionBoundaryPattern = /^#{2,4} /;
240
+
241
+ function fenceMarkerOf(trimmedLine: string): string | null {
242
+ return fenceMarkers.find((marker) => trimmedLine.startsWith(marker)) ?? null;
243
+ }
244
+
245
+ function linesOutsideFences(lines: string[]): boolean[] {
246
+ let openFence: string | null = null;
247
+ return lines.map((line) => {
248
+ const marker = fenceMarkerOf(line.trim());
249
+ if (openFence === null && marker !== null) {
250
+ openFence = marker;
251
+ return false;
252
+ }
253
+ if (openFence !== null) {
254
+ if (marker === openFence) openFence = null;
255
+ return false;
256
+ }
257
+ return true;
258
+ });
259
+ }
260
+
261
+ export function sectionContent(body: string, heading: string): string {
262
+ const lines = body.split("\n");
263
+ const outside = linesOutsideFences(lines);
264
+ const marker = `### ${heading}`;
265
+ const start = lines.findIndex((line, index) => outside[index] && line.trim() === marker);
266
+ if (start === -1) return "";
267
+ const relativeEnd = lines
268
+ .slice(start + 1)
269
+ .findIndex(
270
+ (line, offset) =>
271
+ outside[start + 1 + offset] && sectionBoundaryPattern.test(line.trim()),
272
+ );
273
+ const end = relativeEnd === -1 ? lines.length : start + 1 + relativeEnd;
274
+ return lines
275
+ .slice(start + 1, end)
276
+ .join("\n")
277
+ .replace(/<!--[\s\S]*?-->/g, "")
278
+ .trim();
279
+ }
280
+
281
+ // Cells may contain escaped pipes ("\|"); only unescaped pipes separate cells.
282
+ function tableBodyRows(markdown: string): string[][] {
283
+ const headerAndSeparator = 2;
284
+ return markdown
285
+ .split("\n")
286
+ .filter((line) => line.trim().startsWith("|"))
287
+ .slice(headerAndSeparator)
288
+ .map((line) =>
289
+ line
290
+ .split(/(?<!\\)\|/)
291
+ .slice(1, -1)
292
+ .map((cell) => cell.trim()),
293
+ );
294
+ }
295
+
296
+ function unansweredQuestionRows(body: string): string[][] {
297
+ return tableBodyRows(sectionContent(body, "Open Questions")).filter(
298
+ ([, question, , answer]) => Boolean(question) && !answer,
299
+ );
300
+ }
301
+
302
+ export function unansweredQuestions(body: string): string[] {
303
+ return unansweredQuestionRows(body).map(([id, question]) => id || question);
304
+ }
305
+
306
+ // ID and text of each unanswered question, so it can be asked without
307
+ // re-reading the issue.
308
+ export function unansweredQuestionTexts(body: string): string[] {
309
+ return unansweredQuestionRows(body).map(([id, question]) =>
310
+ id ? `${id} ${question}` : question,
311
+ );
312
+ }
313
+
314
+ function missingPlanFailures(issue: Issue): string[] {
315
+ return sectionContent(issue.body, "LLM Plan")
316
+ ? []
317
+ : [`#${issue.number} has no saved plan (run yaawn-save-plan)`];
318
+ }
319
+
320
+ function unansweredQuestionFailures(issue: Issue): string[] {
321
+ const missing = unansweredQuestions(issue.body);
322
+ return missing.length === 0
323
+ ? []
324
+ : [`#${issue.number} unanswered questions: ${issueRefs(missing, "Q#")}`];
325
+ }
326
+
327
+ type DependencyState = "open" | "closed" | { failure: string };
328
+
329
+ // Fails closed: a dependency whose state cannot be read blocks the issue.
330
+ async function dependencyState(issue: Issue, dependency: number): Promise<DependencyState> {
331
+ try {
332
+ return (await isOpenAsync(dependency)) ? "open" : "closed";
333
+ } catch (error) {
334
+ return {
335
+ failure: `#${issue.number} could not check dependency #${dependency}: ${firstLine(errorMessage(error))}`,
336
+ };
337
+ }
338
+ }
339
+
340
+ // Sync: the readiness checks of implement-plan's pre-flight, single and multi-issue.
341
+ function openExternalDependencyFailures(issue: Issue): string[] {
342
+ const openExternal = issue.externalDependsOn.filter(isOpen);
343
+ return openExternal.length === 0
344
+ ? []
345
+ : [
346
+ `#${issue.number} depends on open issues outside this run: ${issueRefs(openExternal)}`,
347
+ ];
348
+ }
349
+
350
+ export function issueFailures(issue: Issue): string[] {
351
+ return [
352
+ ...missingPlanFailures(issue),
353
+ ...unansweredQuestionFailures(issue),
354
+ ...openExternalDependencyFailures(issue),
355
+ ];
356
+ }
357
+
358
+ // Async: every dependency lookup runs to completion, so all failures show at once.
359
+ async function openExternalDependencyFailuresAsync(issue: Issue): Promise<string[]> {
360
+ const states = await Promise.all(
361
+ issue.externalDependsOn.map((dependency) => dependencyState(issue, dependency)),
362
+ );
363
+ const openExternal = issue.externalDependsOn.filter((_, index) => states[index] === "open");
364
+ const lookupFailures = states.flatMap((state) =>
365
+ typeof state === "object" ? [state.failure] : [],
366
+ );
367
+ return openExternal.length === 0
368
+ ? lookupFailures
369
+ : [
370
+ `#${issue.number} depends on open issues outside this run: ${issueRefs(openExternal)}`,
371
+ ...lookupFailures,
372
+ ];
373
+ }
374
+
375
+ export async function issueFailuresAsync(issue: Issue): Promise<string[]> {
376
+ return [
377
+ ...missingPlanFailures(issue),
378
+ ...unansweredQuestionFailures(issue),
379
+ ...(await openExternalDependencyFailuresAsync(issue)),
380
+ ];
381
+ }