@yaawn/workflow 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -2
- package/agents/instructions/output-styles/yaawn-eli5.instructions.md +45 -0
- package/agents/instructions/yaawn-accessibility.css.instructions.md +58 -0
- package/agents/instructions/yaawn-accessibility.html.instructions.md +90 -0
- package/agents/instructions/yaawn-accessibility.javascript.instructions.md +50 -0
- package/agents/instructions/yaawn-clean-code.typescript.instructions.md +74 -0
- package/agents/instructions/yaawn-function-coupling.typescript.instructions.md +100 -0
- package/agents/instructions/yaawn-html-css-js.html.instructions.md +32 -0
- package/agents/instructions/yaawn-open-source-first.instructions.md +38 -0
- package/agents/instructions/yaawn-personas.instructions.md +18 -0
- package/agents/instructions/yaawn-semantic-html.html.instructions.md +40 -0
- package/agents/instructions/yaawn-vanilla-css.css.instructions.md +50 -0
- package/agents/output-styles/yaawn-eli5.instructions.md +45 -0
- package/agents/personas/yaawn-developer.md +16 -0
- package/agents/personas/yaawn-product-owner.md +18 -0
- package/agents/personas/yaawn-reviewer.md +16 -0
- package/agents/skills/yaawn-clean-code/SKILL.md +61 -0
- package/agents/skills/yaawn-clean-code/references/classes.md +114 -0
- package/agents/skills/yaawn-clean-code/references/comments.md +164 -0
- package/agents/skills/yaawn-clean-code/references/error-handling.md +151 -0
- package/agents/skills/yaawn-clean-code/references/formatting.md +146 -0
- package/agents/skills/yaawn-clean-code/references/functions.md +261 -0
- package/agents/skills/yaawn-clean-code/references/meaningful-names.md +234 -0
- package/agents/skills/yaawn-clean-code/references/objects-and-data-structures.md +130 -0
- package/agents/skills/yaawn-clean-code/references/principles.md +76 -0
- package/agents/skills/yaawn-clean-code/references/simple-design-and-code-smells.md +130 -0
- package/agents/skills/yaawn-clean-code/references/unit-tests.md +114 -0
- package/agents/skills/yaawn-create-plan/SKILL.md +228 -0
- package/agents/skills/yaawn-grill-bug-report/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-bug-report/references/bug-report-checklist.md +27 -0
- package/agents/skills/yaawn-grill-design/SKILL.md +68 -0
- package/agents/skills/yaawn-grill-design/references/design-checklist.md +36 -0
- package/agents/skills/yaawn-grill-other/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-other/references/other-checklist.md +19 -0
- package/agents/skills/yaawn-grill-refactor/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-refactor/references/refactor-checklist.md +26 -0
- package/agents/skills/yaawn-grill-spike/SKILL.md +31 -0
- package/agents/skills/yaawn-grill-spike/references/spike-checklist.md +25 -0
- package/agents/skills/yaawn-grill-story/SKILL.md +34 -0
- package/agents/skills/yaawn-grill-story/references/concept-challenge.md +67 -0
- package/agents/skills/yaawn-grill-story/references/story-checklist.md +43 -0
- package/agents/skills/yaawn-implement-plan/SKILL.md +196 -0
- package/agents/skills/yaawn-implement-plan/assets/fix-prompt.md +66 -0
- package/agents/skills/yaawn-implement-plan/assets/review-prompt.md +89 -0
- package/agents/skills/yaawn-implement-plan/assets/session-prompt.md +80 -0
- package/agents/skills/yaawn-implement-plan/scripts/agent.ts +218 -0
- package/agents/skills/yaawn-implement-plan/scripts/caffeinate.ts +56 -0
- package/agents/skills/yaawn-implement-plan/scripts/groups.ts +233 -0
- package/agents/skills/yaawn-implement-plan/scripts/overnight.ts +772 -0
- package/agents/skills/yaawn-implement-plan/scripts/parallel.ts +1176 -0
- package/agents/skills/yaawn-implement-plan/scripts/processes.ts +53 -0
- package/agents/skills/yaawn-implement-plan/scripts/stages.ts +65 -0
- package/agents/skills/yaawn-implement-plan/scripts/targets.ts +381 -0
- package/agents/skills/yaawn-implement-plan/scripts/worktrees.ts +431 -0
- package/agents/skills/yaawn-owasp-security/SKILL.md +106 -0
- package/agents/skills/yaawn-owasp-security/references/a01-broken-access-control.md +58 -0
- package/agents/skills/yaawn-owasp-security/references/a02-security-misconfiguration.md +56 -0
- package/agents/skills/yaawn-owasp-security/references/a03-software-supply-chain-failures.md +56 -0
- package/agents/skills/yaawn-owasp-security/references/a04-cryptographic-failures.md +67 -0
- package/agents/skills/yaawn-owasp-security/references/a05-injection.md +53 -0
- package/agents/skills/yaawn-owasp-security/references/a06-insecure-design.md +48 -0
- package/agents/skills/yaawn-owasp-security/references/a07-authentication-failures.md +68 -0
- package/agents/skills/yaawn-owasp-security/references/a08-software-data-integrity-failures.md +45 -0
- package/agents/skills/yaawn-owasp-security/references/a09-security-logging-alerting-failures.md +59 -0
- package/agents/skills/yaawn-owasp-security/references/a10-mishandling-exceptional-conditions.md +63 -0
- package/agents/skills/yaawn-prepare-plan/SKILL.md +110 -0
- package/agents/skills/yaawn-save-plan/SKILL.md +65 -0
- package/agents/skills/yaawn-save-plan/scripts/plan-body.ts +331 -0
- package/agents/skills/yaawn-save-plan/scripts/save-plan.ts +443 -0
- package/agents/skills/yaawn-split-story/SKILL.md +189 -0
- package/agents/skills/yaawn-split-story/references/spider.md +64 -0
- package/agents/skills/yaawn-split-story/scripts/create-issue.ts +162 -0
- package/agents/skills/yaawn-verify-executed-plan/SKILL.md +154 -0
- package/agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts +362 -0
- package/agents/skills/yaawn-verify-executed-plan/scripts/pull-request.ts +131 -0
- package/agents/skills/yaawn-write-bug-report/SKILL.md +87 -0
- package/agents/skills/yaawn-write-other/SKILL.md +90 -0
- package/agents/skills/yaawn-write-refactor/SKILL.md +94 -0
- package/agents/skills/yaawn-write-spike/SKILL.md +94 -0
- package/agents/skills/yaawn-write-user-story/SKILL.md +187 -0
- package/agents/skills/yaawn-write-user-story/references/acceptance-criteria.md +186 -0
- package/agents/skills/yaawn-write-user-story/references/five-types-of-value.md +82 -0
- package/agents/skills/yaawn-write-user-story/references/user-story-fundamentals.md +75 -0
- package/agents/yaawn-glossary.md +146 -0
- package/agents/yaawn-grilling.md +75 -0
- package/agents/yaawn-worktrees.md +69 -0
- package/config/install-tasks.json +35 -0
- package/config/settings.json +23 -0
- package/config/wizard.json +128 -0
- package/github/ISSUE_TEMPLATE/yaawn-bug-report.md +59 -0
- package/github/ISSUE_TEMPLATE/yaawn-other.md +38 -0
- package/github/ISSUE_TEMPLATE/yaawn-refactor.md +38 -0
- package/github/ISSUE_TEMPLATE/yaawn-spike.md +16 -0
- package/github/ISSUE_TEMPLATE/yaawn-story.md +56 -0
- package/package.json +33 -4
- package/scripts/install.js +73 -0
- package/yaawn/gitignore +1 -0
- package/yaawn/yaawn-definition-of-done.md +5 -0
- package/yaawn/yaawn-project-glossary.md +7 -0
- package/yaawn/yaawn-user-roles.md +3 -0
- package/yaawn/yaawn-verification-commands.md +7 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
interface Options {
|
|
5
|
+
original: number;
|
|
6
|
+
template: string;
|
|
7
|
+
title: string;
|
|
8
|
+
bodyFile: string;
|
|
9
|
+
dependsOn: number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const templatesDir = ".github/ISSUE_TEMPLATE";
|
|
13
|
+
const plansDir = ".yaawn/plans";
|
|
14
|
+
const dependenciesHeading = "## Dependencies";
|
|
15
|
+
const implementationPlanHeading = "## Implementation Plan";
|
|
16
|
+
const usage =
|
|
17
|
+
"Usage: npx tsx .agents/skills/yaawn-split-story/scripts/create-issue.ts <original> --template <file> --title <title> --body-file <path> [--depends-on <n>[,<n>...]]";
|
|
18
|
+
|
|
19
|
+
const issueBodyPath = (original: number): string =>
|
|
20
|
+
`${plansDir}/yaawn-${original}-split-issue-body.md`;
|
|
21
|
+
|
|
22
|
+
function gh(args: string[]): string {
|
|
23
|
+
return execFileSync("gh", args, { encoding: "utf-8" }).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseFlags(args: string[]): Map<string, string> {
|
|
27
|
+
if (args.length % 2 !== 0) throw new Error(usage);
|
|
28
|
+
const flags = new Map<string, string>();
|
|
29
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
30
|
+
const [flag, value] = args.slice(index, index + 2);
|
|
31
|
+
if (!flag.startsWith("--")) throw new Error(usage);
|
|
32
|
+
flags.set(flag.slice(2), value);
|
|
33
|
+
}
|
|
34
|
+
return flags;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseDependsOn(value: string | undefined): number[] {
|
|
38
|
+
if (value === undefined) return [];
|
|
39
|
+
if (!/^#?\d+(,\s*#?\d+)*$/.test(value)) throw new Error(usage);
|
|
40
|
+
return value.split(",").map((issue) => Number(issue.trim().replace("#", "")));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseOptions(argv: string[]): Options {
|
|
44
|
+
const [originalArg, ...rest] = argv;
|
|
45
|
+
if (!/^\d+$/.test(originalArg ?? "")) throw new Error(usage);
|
|
46
|
+
const flags = parseFlags(rest);
|
|
47
|
+
const required = ["template", "title", "body-file"].map((name) => {
|
|
48
|
+
const value = flags.get(name);
|
|
49
|
+
if (!value) throw new Error(`--${name} is required\n${usage}`);
|
|
50
|
+
return value;
|
|
51
|
+
});
|
|
52
|
+
const [template, title, bodyFile] = required;
|
|
53
|
+
const original = Number(originalArg);
|
|
54
|
+
const dependsOn = parseDependsOn(flags.get("depends-on"));
|
|
55
|
+
if (dependsOn.includes(original)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`--depends-on must not list the original #${original} — the new issues replace it`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return { original, template, title, bodyFile, dependsOn };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function frontmatterValue(template: string, key: string): string {
|
|
64
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(template)?.[1] ?? "";
|
|
65
|
+
const line = frontmatter
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.find((candidate) => candidate.startsWith(`${key}:`));
|
|
68
|
+
return line?.slice(key.length + 1).trim() ?? "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Accepts both YAML list styles the templates use:
|
|
72
|
+
// labels: ["Yaawn: Story", "Yaawn: To Be Refined"] and labels: Yaawn: Spike, Yaawn: To Be Refined
|
|
73
|
+
function listValue(value: string): string[] {
|
|
74
|
+
return value
|
|
75
|
+
.replace(/^\[|\]$/g, "")
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((item) => item.trim().replace(/^["']|["']$/g, ""))
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readTemplate(name: string): { labels: string[]; projects: string[] } {
|
|
82
|
+
const path = `${templatesDir}/${name}`;
|
|
83
|
+
if (!existsSync(path)) throw new Error(`template not found: ${path}`);
|
|
84
|
+
const template = readFileSync(path, "utf-8");
|
|
85
|
+
return {
|
|
86
|
+
labels: listValue(frontmatterValue(template, "labels")),
|
|
87
|
+
projects: listValue(frontmatterValue(template, "projects")),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function dependenciesSection(dependsOn: number[]): string {
|
|
92
|
+
const declaration =
|
|
93
|
+
dependsOn.length > 0
|
|
94
|
+
? `**Depends on:** ${dependsOn.map((issue) => `#${issue}`).join(", ")}`
|
|
95
|
+
: "None — this can be worked on independently.";
|
|
96
|
+
return `${dependenciesHeading}\n\n${declaration}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The overnight run parses this section, so the script writes it — never the agent.
|
|
100
|
+
function withDependencies(body: string, dependsOn: number[]): string {
|
|
101
|
+
if (body.split(/\r?\n/).some((line) => line.trim() === dependenciesHeading)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`the body already has "${dependenciesHeading}" — remove it and pass --depends-on instead`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const section = dependenciesSection(dependsOn);
|
|
107
|
+
const planStart = body.indexOf(`\n${implementationPlanHeading}`);
|
|
108
|
+
if (planStart === -1) return `${body.trimEnd()}\n\n${section}\n`;
|
|
109
|
+
return `${body.slice(0, planStart).trimEnd()}\n\n${section}\n${body.slice(planStart)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createIssue(options: Options, bodyPath: string): number {
|
|
113
|
+
const { labels, projects } = readTemplate(options.template);
|
|
114
|
+
const url = gh([
|
|
115
|
+
"issue",
|
|
116
|
+
"create",
|
|
117
|
+
"--title",
|
|
118
|
+
options.title,
|
|
119
|
+
"--body-file",
|
|
120
|
+
bodyPath,
|
|
121
|
+
...labels.flatMap((label) => ["--label", label]),
|
|
122
|
+
...projects.flatMap((project) => ["--project", project]),
|
|
123
|
+
]);
|
|
124
|
+
const number = /\/issues\/(\d+)\s*$/.exec(url)?.[1];
|
|
125
|
+
if (!number) throw new Error(`could not read the issue number from: ${url}`);
|
|
126
|
+
console.log(`created #${number} ${url}`);
|
|
127
|
+
return Number(number);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function commentOrigin(original: number, issue: number): void {
|
|
131
|
+
gh([
|
|
132
|
+
"issue",
|
|
133
|
+
"comment",
|
|
134
|
+
String(issue),
|
|
135
|
+
"--body",
|
|
136
|
+
`Split from #${original}, which this issue and its sibling slices replace. The split rationale is in a comment on #${original}.`,
|
|
137
|
+
]);
|
|
138
|
+
console.log(`commented the origin #${original} on #${issue}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function main(): void {
|
|
142
|
+
const options = parseOptions(process.argv.slice(2));
|
|
143
|
+
const body = readFileSync(options.bodyFile, "utf-8");
|
|
144
|
+
mkdirSync(plansDir, { recursive: true });
|
|
145
|
+
const bodyPath = issueBodyPath(options.original);
|
|
146
|
+
writeFileSync(bodyPath, withDependencies(body, options.dependsOn));
|
|
147
|
+
const issue = createIssue(options, bodyPath);
|
|
148
|
+
try {
|
|
149
|
+
commentOrigin(options.original, issue);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`#${issue} was created but the origin comment failed — add it by hand, do not create the issue again: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
main();
|
|
159
|
+
} catch (error: unknown) {
|
|
160
|
+
console.error(error instanceof Error ? error.message : error);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yaawn-verify-executed-plan
|
|
3
|
+
description: "Final verification gate of the plan pipeline, run by the developer after /yaawn-implement-plan finishes — once per issue. Assigns every Definition of Done step from .yaawn/yaawn-definition-of-done.md to a check, launches the project's verification commands from .yaawn/yaawn-verification-commands.md (set up by the install script, optional) as an unattended overnight task (kept awake with caffeinate, logs in .yaawn/plans/), lists the manual checks, and — once verified — opens a pull request for the issue."
|
|
4
|
+
argument-hint: "GitHub issue number"
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
metadata:
|
|
7
|
+
github-repo: https://github.com/markusfalk/yaawn
|
|
8
|
+
author: markusfalk
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Verify Executed Plan
|
|
12
|
+
|
|
13
|
+
The last skill of the plan pipeline: `/yaawn-prepare-plan` → `/yaawn-create-plan` → `/yaawn-save-plan` → `/yaawn-implement-plan` → `/yaawn-verify-executed-plan`. Start it in a new session: it works only from the issue and the files under `.yaawn/plans/`, never from an earlier conversation. The developer runs it after `/yaawn-implement-plan` has finished. Like `/yaawn-implement-plan`, the long work runs as an unattended overnight task — when the project has verification commands configured: they run in a terminal kept awake by `caffeinate`, and the developer reads the results in the morning. Both `.yaawn/yaawn-verification-commands.md` and `.yaawn/yaawn-definition-of-done.md` are optional; without them, verification is the manual checklist only. It runs the verification steps that belong after execution — checking the plan itself is the earlier skills' job.
|
|
14
|
+
|
|
15
|
+
It takes the same issue number as `/yaawn-implement-plan` and resolves the same **target**: the issue itself. Every issue is verified on its own.
|
|
16
|
+
|
|
17
|
+
Out of scope: implementing or fixing anything, committing or pushing, writing or editing plans, editing the issue — step 7 is the only exception: it opens a pull request for the issue's branch and removes the issue's `Yaawn: Ready To Implement` and `Yaawn: implementation-group-<n>` labels. On failure, report and stop — fixing is `/yaawn-implement-plan`'s job.
|
|
18
|
+
|
|
19
|
+
Ask for the issue number if it is not given. Run all commands from the repo root.
|
|
20
|
+
|
|
21
|
+
## 1. Pre-flight
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx tsx .agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts preflight <issue>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It first checks where it runs. `/yaawn-implement-plan` runs every issue in its own git worktree inside the project (`<project>/yaawn-<issue>-<slug>`, branch `yaawn/<issue>`), next to the main copy `<project>/main` — the bare-repo layout described in [`.agents/yaawn-worktrees.md`](../../yaawn-worktrees.md):
|
|
28
|
+
|
|
29
|
+
- The issue has its own worktree and the session runs elsewhere: `FAIL` naming the worktree's path.
|
|
30
|
+
- The issue has no worktree of its own and the session runs in another issue's worktree: `FAIL` pointing to the main copy `<project>/main`.
|
|
31
|
+
- The issue has no worktree of its own (implemented before every run got one, or by hand) and the session runs in the main copy: `WARN #<issue> has no git worktree of its own — verifying the main copy`. Mention the warning to the developer and continue.
|
|
32
|
+
|
|
33
|
+
It prints the target, the verification commands — or a note when none are configured — and checks `gh`, `caffeinate`, that no verification is already running for the issue, and that the target's plan is fully executed — every step in `.yaawn/plans/yaawn-<target>-progress.md` is `- [x]`. It also lists uncommitted changes (files under `.yaawn/plans/` are left out), because the suites run against the working tree.
|
|
34
|
+
|
|
35
|
+
- Any `FAIL` line: stop and report it. A worktree `FAIL` means: start a new session in the named directory and run this skill there. A plan that is not fully executed means `/yaawn-implement-plan` has not finished — finish it first.
|
|
36
|
+
- Uncommitted changes listed: show them and ask whether to continue.
|
|
37
|
+
- No verification commands configured: skip steps 3 and 4 — the manual checks are the verification.
|
|
38
|
+
|
|
39
|
+
## 2. Assign every Definition of Done step
|
|
40
|
+
|
|
41
|
+
`.yaawn/yaawn-definition-of-done.md` is the single source of truth for the standard steps — never read Definition of Done steps from the llm-plan file or the issue. If the file is missing or lists no steps, there are no standard steps to assign.
|
|
42
|
+
|
|
43
|
+
For each target, read `.yaawn/plans/yaawn-<target>-progress.md` to see which steps the overnight session already checked off or listed as manual checks.
|
|
44
|
+
|
|
45
|
+
Every standard Definition of Done step must, for every target, be accounted for in exactly one of three places:
|
|
46
|
+
|
|
47
|
+
- **Done in the progress log** — the overnight session turned a code-producing item (e.g. a regression test, API documentation, translations) into a step and checked it off.
|
|
48
|
+
- **Covered by the verification commands** (step 3) — a step about tests passing.
|
|
49
|
+
- **A manual check** (step 5) — every other step, keeping its wording and condition from the docs file (e.g. "if the UI changed"). The overnight session already lists verify-only items under the progress log's "Manual checks"; do not list them twice.
|
|
50
|
+
|
|
51
|
+
With no verification commands configured, a tests-passing step becomes a manual check too — the developer runs the suites by hand.
|
|
52
|
+
|
|
53
|
+
## 3. Launch the overnight verification
|
|
54
|
+
|
|
55
|
+
This step only applies when pre-flight printed verification commands.
|
|
56
|
+
|
|
57
|
+
An orchestrator ([scripts/overnight.ts](./scripts/overnight.ts)) runs the verification unattended in a terminal, kept awake by `caffeinate` — the same shape as `/yaawn-implement-plan`. All suite output streams to that terminal, each line prefixed `[#<issue>]`, and every suite also writes a log under `.yaawn/plans/`.
|
|
58
|
+
|
|
59
|
+
The orchestrator runs the commands in `.yaawn/yaawn-verification-commands.md`, in order — the skill does not assume, propose, or change a project's verification commands.
|
|
60
|
+
|
|
61
|
+
### The verification commands
|
|
62
|
+
|
|
63
|
+
The developer sets the commands up once, when running the install script (`npx @yaawn/workflow`). It adds them in run order to `.yaawn/yaawn-verification-commands.md`, one bullet per command — the same way it fills `.yaawn/yaawn-definition-of-done.md`:
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
- `<shell command>`
|
|
67
|
+
- `<shell command>`
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- Every `- ` bullet is one command, run through the shell. Backticks around the command are optional; any other line is ignored.
|
|
71
|
+
- The n-th command logs to `.yaawn/plans/yaawn-<issue>-verify-step-<n>.log`.
|
|
72
|
+
|
|
73
|
+
Do not write or edit this file. To enable the overnight verification, re-run `npx @yaawn/workflow` or edit the file.
|
|
74
|
+
|
|
75
|
+
### Launch
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
NO_COLOR=1 npx tsx .agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts run <issue>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Run it in an **async** terminal so it keeps running in the editor. Always set `NO_COLOR=1`: you relay its output into the chat, where color codes show as junk. Launch it exactly once. At launch, it deletes the summary of any earlier verification, so a leftover summary never looks current. Read the initial output:
|
|
82
|
+
|
|
83
|
+
- Walk-away banner followed by the first suite starting: launched. The banner appears only once `caffeinate` is active.
|
|
84
|
+
- The process exited or an error is shown: failed launch. Report the lines and stop. Do not retry with other flags.
|
|
85
|
+
|
|
86
|
+
The orchestrator runs the verification commands in order and **stops at the first failure** — the remaining suites are `skipped`.
|
|
87
|
+
|
|
88
|
+
Gotchas:
|
|
89
|
+
|
|
90
|
+
- `caffeinate -s` only prevents system sleep on AC power. That is why the hand-off says to keep the Mac on power.
|
|
91
|
+
- Closing the editor or the terminal ends the run. Leave both open overnight.
|
|
92
|
+
- If a run dies, run `run` again — the suites simply rerun.
|
|
93
|
+
|
|
94
|
+
### Start message
|
|
95
|
+
|
|
96
|
+
Reply with this template, filled in:
|
|
97
|
+
|
|
98
|
+
```markdown
|
|
99
|
+
Overnight verification for #<issue> started in the terminal.
|
|
100
|
+
|
|
101
|
+
- Target: #<issue>
|
|
102
|
+
- Suites: the verification commands from `.yaawn/yaawn-verification-commands.md` — stops at the first failure
|
|
103
|
+
- Output: the launch terminal, lines prefixed `[#<issue>]`; ends with each suite's state and the summary
|
|
104
|
+
- Stop everything: Ctrl+C in that terminal (the running suite and caffeinate end with it)
|
|
105
|
+
- Results: `.yaawn/plans/yaawn-<issue>-verify-summary.md`, per-suite logs `.yaawn/plans/yaawn-<issue>-verify-step-<n>.log`
|
|
106
|
+
|
|
107
|
+
Keep the Mac on power and the editor open. I will report the results here when the run ends.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then end your turn and wait. Do not poll the terminal or the summary file — the harness notifies you when the background process ends. Then continue with step 4.
|
|
111
|
+
|
|
112
|
+
**When the developer asks about the verification later**, in this chat or a new one, check in this order and stop at the first match:
|
|
113
|
+
|
|
114
|
+
1. `pgrep -f "yaawn-verify-executed-plan/scripts/overnight.ts run <issue>( |$)"` finds a process → say the verification is still running. Never report a summary while the run is active.
|
|
115
|
+
2. `.yaawn/plans/yaawn-<issue>-verify-summary.md` exists → continue with step 4.
|
|
116
|
+
3. Otherwise → say the verification was stopped before it ended and that launching it again reruns it.
|
|
117
|
+
|
|
118
|
+
## 4. Report the results
|
|
119
|
+
|
|
120
|
+
This step only applies when the overnight verification ran.
|
|
121
|
+
|
|
122
|
+
Read `.yaawn/plans/yaawn-<issue>-verify-summary.md`. Report one line per suite — pass/fail and the failing specs only. Do not paste full logs; they are in the per-suite log files.
|
|
123
|
+
|
|
124
|
+
If a suite failed: keep the working files, report the failures, and tell the developer to fix them (resuming `/yaawn-implement-plan` if a step has to be redone) and then start a new session and run this skill again. Do not continue to the manual checks.
|
|
125
|
+
|
|
126
|
+
## 5. Manual checks for the developer
|
|
127
|
+
|
|
128
|
+
The developer verifies by hand. Group the checklist by target. For each target, list:
|
|
129
|
+
|
|
130
|
+
- the "Manual checks" from its progress log, and
|
|
131
|
+
- the Definition of Done steps assigned to a manual check in step 2 that the progress log does not list yet, each with its own wording and condition.
|
|
132
|
+
|
|
133
|
+
## 6. Confirm with the developer
|
|
134
|
+
|
|
135
|
+
When every suite is green — or no suites were configured — present the manual checklist from step 5 and wait for the developer to confirm it. The plan is verified once the developer confirms. Leave the working files under `.yaawn/plans/` in place — the developer cleans them up manually.
|
|
136
|
+
|
|
137
|
+
## 7. Finish the issue
|
|
138
|
+
|
|
139
|
+
The plan is verified. Open the pull request.
|
|
140
|
+
|
|
141
|
+
Tell the developer to commit and push the issue's branch, then — once they confirm — run:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
npx tsx .agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts finish <issue>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
It repeats the worktree check, requires the summary's `Result: passed` when verification commands are configured, and requires a branch other than the default branch, no uncommitted changes outside `.yaawn/plans/`, an upstream, and nothing unpushed. Then it finds the branch's open pull request or opens one (`Closes #<issue>`, assigned to the developer), removes the issue's `Yaawn: Ready To Implement` label together with its `Yaawn: implementation-group-<n>` labels (through `/yaawn-implement-plan`'s shared group-label reset — a failed reset only warns), and prints the link. It never commits or pushes.
|
|
148
|
+
|
|
149
|
+
Relay its output:
|
|
150
|
+
|
|
151
|
+
- `pull request opened: <url>` or `pull request already open: <url>`: report the link. The issue closes when the pull request is merged.
|
|
152
|
+
- Any `FAIL` line: report it and tell the developer what to do — commit and push, or switch to the issue's branch or worktree — and then run this step again.
|
|
153
|
+
|
|
154
|
+
Never commit or push yourself, and never offer to close the issue.
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
import type { Readable } from "node:stream";
|
|
12
|
+
import {
|
|
13
|
+
keepMacAwakeWhileRunning,
|
|
14
|
+
printWalkAwayBanner,
|
|
15
|
+
} from "../../yaawn-implement-plan/scripts/caffeinate.js";
|
|
16
|
+
import {
|
|
17
|
+
type Issue,
|
|
18
|
+
isDone,
|
|
19
|
+
issueRefs,
|
|
20
|
+
plansDir,
|
|
21
|
+
progressPath,
|
|
22
|
+
removeGroupLabels,
|
|
23
|
+
resolveTargets,
|
|
24
|
+
succeeds,
|
|
25
|
+
uncommittedChanges,
|
|
26
|
+
} from "../../yaawn-implement-plan/scripts/targets.js";
|
|
27
|
+
import { checkWorktree } from "../../yaawn-implement-plan/scripts/worktrees.js";
|
|
28
|
+
import {
|
|
29
|
+
createPullRequest,
|
|
30
|
+
currentBranch,
|
|
31
|
+
defaultBranch,
|
|
32
|
+
defaultBranchFailure,
|
|
33
|
+
openPullRequestUrl,
|
|
34
|
+
pushedStateFailures,
|
|
35
|
+
readPushedState,
|
|
36
|
+
readVerifyResult,
|
|
37
|
+
removeReadyLabel,
|
|
38
|
+
} from "./pull-request.js";
|
|
39
|
+
|
|
40
|
+
interface Suite {
|
|
41
|
+
name: string;
|
|
42
|
+
command: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type SuiteState = "passed" | "failed" | "skipped";
|
|
46
|
+
|
|
47
|
+
interface SuiteResult {
|
|
48
|
+
suite: Suite;
|
|
49
|
+
state: SuiteState;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SuiteHandle {
|
|
53
|
+
current: ChildProcess | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const suitesPath = ".yaawn/yaawn-verification-commands.md";
|
|
57
|
+
const suitesSetupHint = `set them up with \`npx @yaawn/workflow\` or edit ${suitesPath}`;
|
|
58
|
+
const exitCodeTerminated = 143;
|
|
59
|
+
const failureTailLines = 40;
|
|
60
|
+
const usage =
|
|
61
|
+
"Usage: npx tsx .agents/skills/yaawn-verify-executed-plan/scripts/overnight.ts <preflight|run|finish|reset-group-labels> <issue>";
|
|
62
|
+
|
|
63
|
+
const logPath = (issue: number, suite: string): string =>
|
|
64
|
+
`${plansDir}/yaawn-${issue}-verify-${suite}.log`;
|
|
65
|
+
|
|
66
|
+
const summaryPath = (issue: number): string =>
|
|
67
|
+
`${plansDir}/yaawn-${issue}-verify-summary.md`;
|
|
68
|
+
|
|
69
|
+
function isRunActive(issue: number): boolean {
|
|
70
|
+
return succeeds("pgrep", [
|
|
71
|
+
"-f",
|
|
72
|
+
`yaawn-verify-executed-plan/scripts/overnight.ts run ${issue}( |$)`,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toolFailures(issue: number): string[] {
|
|
77
|
+
const checks: [string, boolean][] = [
|
|
78
|
+
["gh not authenticated", succeeds("gh", ["auth", "status"])],
|
|
79
|
+
["caffeinate not installed", succeeds("which", ["caffeinate"])],
|
|
80
|
+
[`verification run already active for #${issue}`, !isRunActive(issue)],
|
|
81
|
+
];
|
|
82
|
+
return checks.filter(([, ok]) => !ok).map(([failure]) => failure);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function executionFailures({ number }: Issue): string[] {
|
|
86
|
+
if (!existsSync(progressPath(number))) {
|
|
87
|
+
return [`#${number} has no progress log — run /yaawn-implement-plan first`];
|
|
88
|
+
}
|
|
89
|
+
return isDone(number)
|
|
90
|
+
? []
|
|
91
|
+
: [
|
|
92
|
+
`#${number} plan is not fully executed — finish /yaawn-implement-plan first`,
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Mirrors the bullet format the install script writes: "- `command`".
|
|
97
|
+
function parseCommand(line: string): string | null {
|
|
98
|
+
if (!line.startsWith("- ")) return null;
|
|
99
|
+
const item = line.slice("- ".length).trim();
|
|
100
|
+
const wrapped = /^`([^`]+)`$/.exec(item);
|
|
101
|
+
return wrapped ? wrapped[1] : item || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function loadSuites(): Suite[] {
|
|
105
|
+
if (!existsSync(suitesPath)) return [];
|
|
106
|
+
const commands = readFileSync(suitesPath, "utf-8")
|
|
107
|
+
.split("\n")
|
|
108
|
+
.map(parseCommand)
|
|
109
|
+
.filter((command): command is string => command !== null);
|
|
110
|
+
return commands.map((command, index) => ({
|
|
111
|
+
name: `step-${index + 1}`,
|
|
112
|
+
command,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function printTargets(targets: Issue[]): void {
|
|
117
|
+
console.log("targets:");
|
|
118
|
+
targets.forEach(({ number, title }) =>
|
|
119
|
+
console.log(` #${number} ${title} — ${progressPath(number)}`),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printSuites(suites: Suite[]): void {
|
|
124
|
+
if (suites.length === 0) {
|
|
125
|
+
console.log(
|
|
126
|
+
`no verification commands configured — overnight verification skipped (${suitesSetupHint})`,
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
console.log(`verification commands (${suitesPath}):`);
|
|
131
|
+
suites.forEach((suite, index) =>
|
|
132
|
+
console.log(` ${index + 1}. ${suite.name}: ${suite.command}`),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printFailures(failures: string[]): void {
|
|
137
|
+
failures.forEach((failure) => console.log(`FAIL ${failure}`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function printUncommittedChanges(): void {
|
|
141
|
+
console.log(`--- uncommitted changes (without ${plansDir}/)`);
|
|
142
|
+
console.log(uncommittedChanges());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Returns false when the worktree check fails; a warning is printed and passes.
|
|
146
|
+
function worktreeCheckPasses({ number }: Issue): boolean {
|
|
147
|
+
const { status, message } = checkWorktree(number);
|
|
148
|
+
if (status === "fail") {
|
|
149
|
+
printFailures([message]);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
console.log(`${status === "warn" ? "WARN" : "worktree:"} ${message}`);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function preflight(parent: number): boolean {
|
|
157
|
+
const tools = toolFailures(parent);
|
|
158
|
+
if (tools.length > 0) {
|
|
159
|
+
printFailures(tools);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const targets = resolveTargets(parent);
|
|
164
|
+
if (!worktreeCheckPasses(targets[0])) return false;
|
|
165
|
+
const failures = targets.flatMap(executionFailures);
|
|
166
|
+
printTargets(targets);
|
|
167
|
+
printSuites(loadSuites());
|
|
168
|
+
printFailures(failures);
|
|
169
|
+
printUncommittedChanges();
|
|
170
|
+
return failures.length === 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function streamLines(
|
|
174
|
+
stream: Readable | null,
|
|
175
|
+
issue: number,
|
|
176
|
+
log: string,
|
|
177
|
+
): void {
|
|
178
|
+
if (!stream) return;
|
|
179
|
+
createInterface({ input: stream }).on("line", (line) => {
|
|
180
|
+
console.log(`[#${issue}] ${line}`);
|
|
181
|
+
appendFileSync(log, `${line}\n`);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function runSuite(
|
|
186
|
+
issue: number,
|
|
187
|
+
suite: Suite,
|
|
188
|
+
handle: SuiteHandle,
|
|
189
|
+
): Promise<SuiteState> {
|
|
190
|
+
const log = logPath(issue, suite.name);
|
|
191
|
+
appendFileSync(log, `# ${new Date().toISOString()} ${suite.command}\n\n`);
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
const suiteProcess = spawn(suite.command, {
|
|
194
|
+
shell: true,
|
|
195
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
196
|
+
});
|
|
197
|
+
handle.current = suiteProcess;
|
|
198
|
+
streamLines(suiteProcess.stdout, issue, log);
|
|
199
|
+
streamLines(suiteProcess.stderr, issue, log);
|
|
200
|
+
let settled = false;
|
|
201
|
+
const settle = (state: SuiteState): void => {
|
|
202
|
+
if (settled) return;
|
|
203
|
+
settled = true;
|
|
204
|
+
handle.current = null;
|
|
205
|
+
resolve(state);
|
|
206
|
+
};
|
|
207
|
+
suiteProcess.on("error", (error) => {
|
|
208
|
+
console.log(`[#${issue}] failed to start: ${error.message}`);
|
|
209
|
+
appendFileSync(log, `failed to start: ${error.message}\n`);
|
|
210
|
+
settle("failed");
|
|
211
|
+
});
|
|
212
|
+
suiteProcess.on("exit", (code) => settle(code === 0 ? "passed" : "failed"));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stopSuiteOnSignal(handle: SuiteHandle): void {
|
|
217
|
+
const stop = (): void => {
|
|
218
|
+
handle.current?.kill("SIGTERM");
|
|
219
|
+
process.exit(exitCodeTerminated);
|
|
220
|
+
};
|
|
221
|
+
process.on("SIGTERM", stop);
|
|
222
|
+
process.on("SIGINT", stop);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function tailLines(path: string, count: number): string[] {
|
|
226
|
+
if (!existsSync(path)) return [];
|
|
227
|
+
return readFileSync(path, "utf-8").trimEnd().split("\n").slice(-count);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writeSummary(
|
|
231
|
+
issue: number,
|
|
232
|
+
targets: Issue[],
|
|
233
|
+
results: SuiteResult[],
|
|
234
|
+
): void {
|
|
235
|
+
const lines = [
|
|
236
|
+
`# Verification: #${issue}`,
|
|
237
|
+
"",
|
|
238
|
+
`Targets: ${issueRefs(targets.map(({ number }) => number))}`,
|
|
239
|
+
"",
|
|
240
|
+
`Finished: ${new Date().toISOString()}`,
|
|
241
|
+
"",
|
|
242
|
+
`Result: ${results.every(({ state }) => state === "passed") ? "passed" : "failed"}`,
|
|
243
|
+
"",
|
|
244
|
+
"| Suite | Result | Log |",
|
|
245
|
+
"| --- | --- | --- |",
|
|
246
|
+
...results.map(
|
|
247
|
+
({ suite, state }) =>
|
|
248
|
+
`| ${suite.command} | ${state} | ${logPath(issue, suite.name)} |`,
|
|
249
|
+
),
|
|
250
|
+
"",
|
|
251
|
+
];
|
|
252
|
+
results
|
|
253
|
+
.filter(({ state }) => state === "failed")
|
|
254
|
+
.forEach(({ suite }) => {
|
|
255
|
+
lines.push(
|
|
256
|
+
`## ${suite.name} — failing output (last ${failureTailLines} lines)`,
|
|
257
|
+
"",
|
|
258
|
+
"```",
|
|
259
|
+
...tailLines(logPath(issue, suite.name), failureTailLines),
|
|
260
|
+
"```",
|
|
261
|
+
"",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
writeFileSync(summaryPath(issue), `${lines.join("\n")}\n`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function runSuites(
|
|
268
|
+
issue: number,
|
|
269
|
+
suites: Suite[],
|
|
270
|
+
): Promise<SuiteResult[]> {
|
|
271
|
+
const handle: SuiteHandle = { current: null };
|
|
272
|
+
stopSuiteOnSignal(handle);
|
|
273
|
+
const results: SuiteResult[] = [];
|
|
274
|
+
for (const suite of suites) {
|
|
275
|
+
const alreadyFailed = results.some(({ state }) => state === "failed");
|
|
276
|
+
if (alreadyFailed) {
|
|
277
|
+
results.push({ suite, state: "skipped" });
|
|
278
|
+
console.log(`[#${issue}] ${suite.name} skipped`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
console.log(`\n--- ${suite.name}: ${suite.command}`);
|
|
282
|
+
const state = await runSuite(issue, suite, handle);
|
|
283
|
+
results.push({ suite, state });
|
|
284
|
+
console.log(`[#${issue}] ${suite.name} ${state}`);
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function run(issue: number): Promise<boolean> {
|
|
290
|
+
rmSync(summaryPath(issue), { force: true });
|
|
291
|
+
const suites = loadSuites();
|
|
292
|
+
if (suites.length === 0) {
|
|
293
|
+
console.log(
|
|
294
|
+
`no verification commands configured — nothing to run (${suitesSetupHint})`,
|
|
295
|
+
);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
const targets = resolveTargets(issue);
|
|
299
|
+
await keepMacAwakeWhileRunning();
|
|
300
|
+
printWalkAwayBanner();
|
|
301
|
+
const results = await runSuites(issue, suites);
|
|
302
|
+
|
|
303
|
+
writeSummary(issue, targets, results);
|
|
304
|
+
const passed = results.every(({ state }) => state === "passed");
|
|
305
|
+
console.log("\n--- summary");
|
|
306
|
+
console.log(`#${issue} ${passed ? "passed" : "failed"}`);
|
|
307
|
+
console.log(readFileSync(summaryPath(issue), "utf-8").trim());
|
|
308
|
+
return passed;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function summaryGuardFailures(issue: number): string[] {
|
|
312
|
+
if (loadSuites().length === 0) return [];
|
|
313
|
+
return readVerifyResult(summaryPath(issue)) === "passed"
|
|
314
|
+
? []
|
|
315
|
+
: [`no passing verification for #${issue} in ${summaryPath(issue)} — run the verification first`];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function branchFailures(): string[] {
|
|
319
|
+
const failure = defaultBranchFailure(currentBranch(), defaultBranch());
|
|
320
|
+
if (failure) return [failure];
|
|
321
|
+
return pushedStateFailures(readPushedState(uncommittedChanges()));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function openOrFindPullRequest(issue: number): void {
|
|
325
|
+
const branch = currentBranch();
|
|
326
|
+
const existing = openPullRequestUrl(branch);
|
|
327
|
+
const url = existing ?? createPullRequest(issue, branch, defaultBranch());
|
|
328
|
+
removeReadyLabel(issue);
|
|
329
|
+
removeGroupLabels(issue);
|
|
330
|
+
console.log(`${existing ? "pull request already open" : "pull request opened"}: ${url}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Opens a pull request for the issue's branch; never commits or pushes.
|
|
334
|
+
function finish(issue: number): boolean {
|
|
335
|
+
const targets = resolveTargets(issue);
|
|
336
|
+
if (!worktreeCheckPasses(targets[0])) return false;
|
|
337
|
+
const failures = [...summaryGuardFailures(issue), ...branchFailures()];
|
|
338
|
+
if (failures.length > 0) {
|
|
339
|
+
printFailures(failures);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
openOrFindPullRequest(issue);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function main(): Promise<void> {
|
|
347
|
+
const [command, issueArg, ...extra] = process.argv.slice(2);
|
|
348
|
+
if (!/^[1-9]\d*$/.test(issueArg ?? "") || extra.length > 0) throw new Error(usage);
|
|
349
|
+
const issue = Number(issueArg);
|
|
350
|
+
mkdirSync(plansDir, { recursive: true });
|
|
351
|
+
|
|
352
|
+
if (command === "preflight") process.exitCode = preflight(issue) ? 0 : 1;
|
|
353
|
+
else if (command === "run") process.exitCode = (await run(issue)) ? 0 : 1;
|
|
354
|
+
else if (command === "finish") process.exitCode = finish(issue) ? 0 : 1;
|
|
355
|
+
else if (command === "reset-group-labels") removeGroupLabels(issue);
|
|
356
|
+
else throw new Error(usage);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main().catch((error: unknown) => {
|
|
360
|
+
console.error(error instanceof Error ? error.message : error);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|