@open-tomato/rafa 0.45.0 → 0.46.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 (140) hide show
  1. package/README.md +255 -23
  2. package/dist/PROMPT.md +18 -1
  3. package/dist/adapters/logger/settings.d.ts +132 -0
  4. package/dist/bugs/closest.d.ts +19 -0
  5. package/dist/bugs/fenced.d.ts +8 -0
  6. package/dist/bugs/places.d.ts +20 -0
  7. package/dist/bugs/record.d.ts +145 -0
  8. package/dist/bugs/title.d.ts +83 -0
  9. package/dist/bundled/agents/doc-updater.md +17 -3
  10. package/dist/bundled/agents/loop-implementer.md +14 -2
  11. package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +10 -3
  12. package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +5 -0
  13. package/dist/bundled/skills/dev-planner/SKILL.md +101 -18
  14. package/dist/cli.js +74726 -62960
  15. package/dist/config/entry.d.ts +119 -0
  16. package/dist/config/index.d.ts +36 -0
  17. package/dist/config/index.js +51 -0
  18. package/dist/config/shape.d.ts +232 -0
  19. package/dist/config-registry.d.ts +127 -0
  20. package/dist/config-schema-board-project.d.ts +157 -0
  21. package/dist/config-schema-bugs.d.ts +71 -0
  22. package/dist/config-schema-errors.d.ts +54 -0
  23. package/dist/config-schema-features.d.ts +64 -0
  24. package/dist/config-schema-hub.d.ts +150 -0
  25. package/dist/config-schema-issues.d.ts +95 -0
  26. package/dist/config-schema-logger.d.ts +91 -0
  27. package/dist/config-schema-loop-continue.d.ts +122 -0
  28. package/dist/config-schema-loop-retries.d.ts +94 -0
  29. package/dist/config-schema-release.d.ts +307 -0
  30. package/dist/config-schema-tests.d.ts +170 -0
  31. package/dist/config-schema-triage.d.ts +142 -0
  32. package/dist/config-schema-wrap-up.d.ts +96 -0
  33. package/dist/core/command/command.d.ts +228 -0
  34. package/dist/core/command/confirm.d.ts +67 -0
  35. package/dist/core/command/index.d.ts +112 -0
  36. package/dist/core/command/lazy.d.ts +54 -0
  37. package/dist/core/command/registry-types.d.ts +96 -0
  38. package/dist/core/command/running.d.ts +96 -0
  39. package/dist/core/command/spends.d.ts +39 -0
  40. package/dist/core/command/types.d.ts +92 -0
  41. package/dist/core/command/version.d.ts +4 -0
  42. package/dist/core/config/branch-template.d.ts +20 -0
  43. package/dist/core/config/config-items.d.ts +86 -0
  44. package/dist/core/config/config-load.d.ts +47 -0
  45. package/dist/core/config/config-origins.d.ts +57 -0
  46. package/dist/core/config/config-readers.d.ts +167 -0
  47. package/dist/core/config/config-schema.d.ts +207 -0
  48. package/dist/core/config/config-sections.d.ts +493 -0
  49. package/dist/core/config/config.d.ts +320 -0
  50. package/dist/core/config/core-section.d.ts +39 -0
  51. package/dist/core/config/dangerous.d.ts +58 -0
  52. package/dist/core/config/default-routing.d.ts +16 -0
  53. package/dist/core/config/index.d.ts +141 -0
  54. package/dist/core/config/scope.d.ts +42 -0
  55. package/dist/core/config/section.d.ts +206 -0
  56. package/dist/core/config/sections.d.ts +45 -0
  57. package/dist/core/config/standard-schema.d.ts +27 -0
  58. package/dist/core/git/commit.d.ts +232 -0
  59. package/dist/core/git/gh-runner.d.ts +24 -0
  60. package/dist/core/git/git.d.ts +22 -0
  61. package/dist/core/git/index.d.ts +85 -0
  62. package/dist/core/loop/claude.d.ts +388 -0
  63. package/dist/core/loop/hop-record.d.ts +52 -0
  64. package/dist/core/loop/index.d.ts +179 -0
  65. package/dist/core/loop/session-env.d.ts +20 -0
  66. package/dist/core/loop/session-record-parse.d.ts +63 -0
  67. package/dist/core/loop/sessions.d.ts +408 -0
  68. package/dist/core/loop/suite-failure.d.ts +25 -0
  69. package/dist/core/output/active.d.ts +100 -0
  70. package/dist/core/output/stream.d.ts +20 -0
  71. package/dist/core/output/text.d.ts +55 -0
  72. package/dist/core/plan/block-reader.d.ts +46 -0
  73. package/dist/core/plan/blocks.d.ts +139 -0
  74. package/dist/core/plan/declaration.d.ts +366 -0
  75. package/dist/core/plan/plan-stamp.d.ts +75 -0
  76. package/dist/core/runtime/identity.d.ts +21 -0
  77. package/dist/core/runtime/index.d.ts +46 -0
  78. package/dist/effort/store/bring-forward.d.ts +131 -0
  79. package/dist/effort/store/check-rows.d.ts +31 -0
  80. package/dist/effort/store/commit-stats.d.ts +42 -0
  81. package/dist/effort/store/copy.d.ts +35 -0
  82. package/dist/effort/store/development-build.d.ts +105 -0
  83. package/dist/effort/store/effort-levels.d.ts +13 -0
  84. package/dist/effort/store/findings.d.ts +214 -0
  85. package/dist/effort/store/fix-schema.d.ts +184 -0
  86. package/dist/effort/store/index.js +10 -7
  87. package/dist/effort/store/location.d.ts +44 -0
  88. package/dist/effort/store/merge-commit-gaps.d.ts +68 -0
  89. package/dist/effort/store/merge-conflicts.d.ts +91 -0
  90. package/dist/effort/store/merge-rules.d.ts +82 -0
  91. package/dist/effort/store/merge-store.d.ts +211 -0
  92. package/dist/effort/store/merge-union.d.ts +92 -0
  93. package/dist/effort/store/migrate.d.ts +146 -0
  94. package/dist/effort/store/migrations.d.ts +92 -0
  95. package/dist/effort/store/ndjson-files.d.ts +114 -0
  96. package/dist/effort/store/origins.d.ts +116 -0
  97. package/dist/effort/store/plan-stubs.d.ts +79 -0
  98. package/dist/effort/store/rebuild-aside.d.ts +172 -0
  99. package/dist/effort/store/schema-plan.d.ts +102 -0
  100. package/dist/effort/store/session-row-types.d.ts +64 -0
  101. package/dist/effort/store/settings.d.ts +13 -0
  102. package/dist/effort/store/sqlite.d.ts +340 -0
  103. package/dist/effort/store/store-generation.d.ts +17 -0
  104. package/dist/effort/store/store-identity.d.ts +220 -0
  105. package/dist/effort/store/store-meta.d.ts +305 -0
  106. package/dist/effort/store/store-replaced.d.ts +72 -0
  107. package/dist/effort/store/types.d.ts +223 -0
  108. package/dist/errors/codes.d.ts +56 -0
  109. package/dist/errors/rafa-codes.d.ts +36 -0
  110. package/dist/index-2a7nb3an.js +125 -0
  111. package/dist/{index-r28fzsvc.js → index-39kbcw68.js} +350 -165
  112. package/dist/{index-nv34ptht.js → index-77ajfffn.js} +14 -9
  113. package/dist/index-cy98y87p.js +2597 -0
  114. package/dist/index-gqz4jn6j.js +13447 -0
  115. package/dist/index-gvek4r8m.js +710 -0
  116. package/dist/index-j4k2vdz3.js +1125 -0
  117. package/dist/index-jh1r4kcn.js +1067 -0
  118. package/dist/{index-mgh2s9bj.js → index-ykggr043.js} +58 -455
  119. package/dist/index-z1w83f81.js +4 -0
  120. package/dist/index.js +14715 -21516
  121. package/dist/learning/index.js +5 -6
  122. package/dist/plan/index.js +12 -11
  123. package/dist/plan-prompt.md +3 -1
  124. package/dist/ports/index.d.ts +732 -0
  125. package/dist/ports/learning.d.ts +101 -0
  126. package/dist/ports/log-level.d.ts +13 -0
  127. package/dist/ports/spec-review.d.ts +66 -0
  128. package/dist/pr/checks.d.ts +133 -0
  129. package/dist/pr/types.d.ts +422 -0
  130. package/dist/ready-4f8edezt.js +2016 -0
  131. package/dist/release-templates/release-bot-fragment.yml +153 -0
  132. package/dist/release-templates/release-publish.yml +110 -0
  133. package/dist/release-templates/release-settle.yml +122 -0
  134. package/dist/release-templates/release-tag.yml +102 -0
  135. package/dist/report/parse.d.ts +415 -0
  136. package/dist/schema/project-id.d.ts +79 -0
  137. package/dist/schema/tiers.d.ts +107 -0
  138. package/package.json +41 -3
  139. package/dist/index-hf2k497d.js +0 -16
  140. package/dist/index-nsr78ct2.js +0 -8219
@@ -0,0 +1,2016 @@
1
+ // @bun
2
+ import {
3
+ READINESS_REFUSAL_EXIT,
4
+ REMOTE,
5
+ SPEC_BLOCKED_LABEL,
6
+ SPEC_NEEDS_WORK_LABEL,
7
+ SPEC_READY_LABEL,
8
+ answeredYes,
9
+ blockedLineSentence,
10
+ boardRepoLabel,
11
+ createGhIssueBoard,
12
+ createGhRunner,
13
+ createGhSpecIssueReader,
14
+ createRefreshingGhIssueBoard,
15
+ epicProblemMessage,
16
+ ghBoardTrust,
17
+ goHome,
18
+ hasDiverged,
19
+ hasSpecReadyLabel,
20
+ hopDecisionSentence,
21
+ hopFilePath,
22
+ hopMoveSentence,
23
+ isLive,
24
+ issueSource,
25
+ loadConfig,
26
+ moduleOfLabels,
27
+ notReadySentence,
28
+ oneHopAllows,
29
+ openNextSources,
30
+ parseBaseStanding,
31
+ parseClaimMessage,
32
+ parseWorkingTree,
33
+ planLabel,
34
+ positionAt,
35
+ positionFilePath,
36
+ readBoardRepository,
37
+ readEpicProblems,
38
+ readHopRecord,
39
+ readPositionFile,
40
+ requireCompleteSpec,
41
+ requireTrustedBoardAuthor,
42
+ resolvePrProvider,
43
+ samePlace,
44
+ scanClaimBranches,
45
+ selectBoardRelations,
46
+ staleAgainst,
47
+ trackedChanges,
48
+ typeOfLabels,
49
+ writeHopRecord,
50
+ writePositionFile
51
+ } from "./index-gqz4jn6j.js";
52
+ import {
53
+ countTasks,
54
+ isFile,
55
+ parsePlan,
56
+ planFileName,
57
+ plural,
58
+ stubOfPlanFile
59
+ } from "./index-39kbcw68.js";
60
+ import {
61
+ CommandExit,
62
+ commandSpelling,
63
+ createGitRunner,
64
+ createLinePrompter,
65
+ gitSaid,
66
+ lazyPrompter
67
+ } from "./index-cy98y87p.js";
68
+ import"./index-2a7nb3an.js";
69
+ import"./index-j4k2vdz3.js";
70
+ import {
71
+ ConfigError
72
+ } from "./index-gvek4r8m.js";
73
+ import {
74
+ DEFAULT_BRANCH_TEMPLATE,
75
+ branchFromTemplate,
76
+ messageOf
77
+ } from "./index-ykggr043.js";
78
+ import"./index-z1w83f81.js";
79
+
80
+ // src/cli/core/parseArgs.ts
81
+ var REFUSAL = "parseArgs";
82
+ function refuseArgumentAliases(args) {
83
+ for (const arg of args) {
84
+ if ((arg.aliases ?? []).length > 0) {
85
+ throw new TypeError(`${REFUSAL}: argument "${arg.name}" declares aliases, and a positional argument has no spelling for one to stand for`);
86
+ }
87
+ }
88
+ }
89
+ function spellingsOf(flags) {
90
+ const owners = new Map;
91
+ const claim = (spelling, name) => {
92
+ const owner = owners.get(spelling);
93
+ if (owner !== undefined && owner !== name) {
94
+ throw new TypeError(`${REFUSAL}: the spelling "${spelling}" names both flag "${owner}" and flag "${name}"`);
95
+ }
96
+ owners.set(spelling, name);
97
+ };
98
+ for (const flag of flags)
99
+ claim(flag.name, flag.name);
100
+ for (const flag of flags) {
101
+ for (const alias of flag.aliases ?? [])
102
+ claim(alias, flag.name);
103
+ }
104
+ return owners;
105
+ }
106
+ function readLine(argv, record) {
107
+ const positional = [];
108
+ for (let i = 0;i < argv.length; i++) {
109
+ const word = argv[i];
110
+ if (word === undefined)
111
+ continue;
112
+ if (word === "--") {
113
+ for (const rest of argv.slice(i + 1)) {
114
+ if (rest !== undefined)
115
+ positional.push(rest);
116
+ }
117
+ break;
118
+ }
119
+ const isLong = word.startsWith("--");
120
+ if (!isLong && !(word.startsWith("-") && word.length > 1)) {
121
+ positional.push(word);
122
+ continue;
123
+ }
124
+ const body = word.slice(isLong ? 2 : 1);
125
+ const eqIndex = body.indexOf("=");
126
+ const next = argv[i + 1];
127
+ if (eqIndex !== -1) {
128
+ record(body.slice(0, eqIndex), body.slice(eqIndex + 1));
129
+ } else if (isLong && body.startsWith("no-") && body.length > 3) {
130
+ record(body.slice(3), false);
131
+ } else if (next !== undefined && !next.startsWith("-")) {
132
+ record(body, next);
133
+ i++;
134
+ } else {
135
+ record(body, true);
136
+ }
137
+ }
138
+ return positional;
139
+ }
140
+ function parseArgs(argv, spec = {}) {
141
+ const { args = [], flags: flagSpecs = [] } = spec;
142
+ refuseArgumentAliases(args);
143
+ const spellings = spellingsOf(flagSpecs);
144
+ const flags = {};
145
+ const positional = readLine(argv, (spelling, value) => {
146
+ flags[spellings.get(spelling) ?? spelling] = value;
147
+ });
148
+ for (const flag of flagSpecs) {
149
+ if (flag.default === undefined || Object.hasOwn(flags, flag.name))
150
+ continue;
151
+ flags[flag.name] = typeof flag.default === "number" ? String(flag.default) : flag.default;
152
+ }
153
+ for (const arg of args.slice(positional.length)) {
154
+ if (arg.default === undefined)
155
+ break;
156
+ positional.push(String(arg.default));
157
+ }
158
+ return { positional, flags };
159
+ }
160
+
161
+ // src/next/relations-mode.ts
162
+ async function modeConfig(root, home) {
163
+ try {
164
+ return (await loadConfig({ root, home }, {}, () => {
165
+ return;
166
+ })).config;
167
+ } catch (error) {
168
+ if (error instanceof ConfigError)
169
+ return null;
170
+ throw error;
171
+ }
172
+ }
173
+ async function openNextRelations(context, seams) {
174
+ const { project } = context;
175
+ if (project === null)
176
+ return;
177
+ const config = await modeConfig(project.root, project.home);
178
+ if (config?.boardRelationships !== "native")
179
+ return;
180
+ const provider = resolvePrProvider({ configured: config.prProvider, dir: project.root, readRemote: seams.readRemote });
181
+ if (provider.provider !== "gh")
182
+ return;
183
+ const gh = (seams.openGh ?? ((root) => createGhRunner({ cwd: root })))(project.root);
184
+ const repository = await readBoardRepository(gh);
185
+ return selectBoardRelations(config, { gh, repository });
186
+ }
187
+
188
+ // src/plan/list.ts
189
+ import { existsSync, readdirSync, readFileSync } from "fs";
190
+ import { join } from "path";
191
+ function listing(dir, stub) {
192
+ const plan = join(dir, planFileName(stub, false));
193
+ const trackerPath = join(dir, planFileName(stub, true));
194
+ const tracker = isFile(trackerPath) ? trackerPath : null;
195
+ const planModel = parsePlan(readFileSync(plan, "utf8"));
196
+ const counted = tracker === null ? planModel : parsePlan(readFileSync(tracker, "utf8"));
197
+ return { stub, plan, tracker, tasks: countTasks(counted.tasks), issues: planModel.issues.length };
198
+ }
199
+ function listPlans(plans) {
200
+ const dir = plans.path;
201
+ if (!existsSync(dir))
202
+ return { dir, plans: [] };
203
+ const stubs = readdirSync(dir).filter((name) => isFile(join(dir, name))).map((name) => stubOfPlanFile(name)).filter((stub) => stub !== null).sort((a, b) => a.localeCompare(b));
204
+ return { dir, plans: stubs.map((stub) => listing(dir, stub)) };
205
+ }
206
+
207
+ // src/next/readings.ts
208
+ var DETACHED = "HEAD";
209
+ var PORCELAIN_PATH_AT = 3;
210
+ function once(read) {
211
+ let held = null;
212
+ return () => {
213
+ held = held ?? { value: read() };
214
+ return held.value;
215
+ };
216
+ }
217
+ function readBranch(sources, note) {
218
+ const result = sources.git(["rev-parse", "--abbrev-ref", "HEAD"]);
219
+ if (!result.ok) {
220
+ note(`the branch at the project root could not be read, so no branch names a plan or a pull request: ${gitSaid(result)}`);
221
+ return "";
222
+ }
223
+ const branch = result.stdout.trim();
224
+ if (branch !== DETACHED)
225
+ return branch;
226
+ note("the project root is on a detached HEAD, so no branch names a plan or a pull request");
227
+ return "";
228
+ }
229
+ function readStanding(sources, remote, note) {
230
+ const { base } = sources;
231
+ const tracking = `${remote}/${base}`;
232
+ const fetched = sources.git(["fetch", remote, base]);
233
+ if (!fetched.ok) {
234
+ note(`\`${tracking}\` could not be fetched, so how \`${base}\` stands is read off what this clone holds: ${gitSaid(fetched)}`);
235
+ }
236
+ const counted = sources.git(["rev-list", "--left-right", "--count", `${base}...${tracking}`]);
237
+ if (!counted.ok) {
238
+ note(`how \`${base}\` stands against \`${tracking}\` could not be read: ${gitSaid(counted)}`);
239
+ return null;
240
+ }
241
+ const standing = parseBaseStanding(counted.stdout);
242
+ if (standing === null) {
243
+ note(`git answered ${JSON.stringify(counted.stdout)} for how \`${base}\` stands against \`${tracking}\`, which is no pair of counts`);
244
+ }
245
+ return standing;
246
+ }
247
+ function changedPaths(entries) {
248
+ return Object.freeze(entries.map((entry) => entry.slice(PORCELAIN_PATH_AT).trim()));
249
+ }
250
+ function readTracked(sources, note) {
251
+ const result = sources.git(["status", "--porcelain"]);
252
+ if (!result.ok) {
253
+ note(`the working tree could not be read, so whether it holds changes to tracked files is unknown: ${gitSaid(result)}`);
254
+ return Object.freeze([]);
255
+ }
256
+ return changedPaths(trackedChanges(parseWorkingTree(result.stdout)));
257
+ }
258
+ function readCheckout(sources, note) {
259
+ const result = sources.git(["rev-parse", "--show-toplevel"]);
260
+ const checkout = result.ok ? result.stdout.trim() : "";
261
+ if (checkout !== "")
262
+ return checkout;
263
+ const why = result.ok ? "git printed no top level" : gitSaid(result);
264
+ note(`the checkout the working tree was read in could not be named: ${why}`);
265
+ return null;
266
+ }
267
+ function holdsTree(live, checkout) {
268
+ return live.some((record) => record.worktree === undefined) || live.some((record) => record.worktree === checkout());
269
+ }
270
+ function readRefs(sources, remote, note, scanner = scanClaimBranches) {
271
+ const scan = scanner(sources.git, remote, sources.branchTemplate);
272
+ scan.problems.forEach((problem) => note(problem));
273
+ return scan.refs;
274
+ }
275
+ function readPlans(sources, note) {
276
+ try {
277
+ const listing2 = listPlans(sources.plans);
278
+ return listing2.plans;
279
+ } catch (error) {
280
+ note(`the plans under ${sources.plans.label}/ could not be read: ${messageOf(error)}`);
281
+ return [];
282
+ }
283
+ }
284
+ async function readOpenPull(sources, branch, note) {
285
+ if (branch === "")
286
+ return null;
287
+ const summary = await sources.pulls.findOpen(branch);
288
+ if (summary === null)
289
+ return null;
290
+ const detail = await sources.pulls.get(summary.number);
291
+ if (detail === null) {
292
+ note(`the provider answered no pull request #${summary.number}, having just named it as the open one on \`${branch}\``);
293
+ }
294
+ const checks = await sources.pulls.checks(summary.number);
295
+ return { summary, mergeable: detail?.mergeable ?? "unknown", verdict: checks.verdict };
296
+ }
297
+ function readBranchPlan(branch, plans, template = DEFAULT_BRANCH_TEMPLATE) {
298
+ return plans.find((plan) => branchFromTemplate(template, plan.stub) === branch) ?? null;
299
+ }
300
+ function hasRun(records, stub) {
301
+ const file = planFileName(stub, false);
302
+ return records.some((record) => record.planStub === stub || record.plan === file || record.plan.endsWith(`/${file}`));
303
+ }
304
+ function hasBranch(refs, stub, template = DEFAULT_BRANCH_TEMPLATE) {
305
+ const branch = branchFromTemplate(template, stub);
306
+ return refs.some((ref) => ref === branch || ref.endsWith(`/${branch}`));
307
+ }
308
+ function readBranchTips(sources, remote, stub, note) {
309
+ const branch = planBranchOf(sources, stub);
310
+ const tips = [];
311
+ for (const ref of [`refs/heads/${branch}`, `refs/remotes/${remote}/${branch}`]) {
312
+ const resolved = sources.git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
313
+ const said = gitSaid(resolved);
314
+ if (resolved.ok && resolved.stdout.trim() !== "") {
315
+ tips.push(ref);
316
+ } else if (said !== "") {
317
+ note(`\`${ref}\` could not be read, so its plan reads as started: ${said}`);
318
+ return null;
319
+ }
320
+ }
321
+ return tips;
322
+ }
323
+ function planBranchOf(sources, stub) {
324
+ return branchFromTemplate(sources.branchTemplate ?? DEFAULT_BRANCH_TEMPLATE, stub);
325
+ }
326
+ function readBranchMessages(sources, ref, note) {
327
+ const result = sources.git(["log", "-z", "--format=%B", `${sources.base}..${ref}`]);
328
+ if (!result.ok) {
329
+ note(`the commits of \`${ref}\` past \`${sources.base}\` could not be read, so its plan reads as started: ${gitSaid(result)}`);
330
+ return null;
331
+ }
332
+ return result.stdout.split("\x00").map((message) => message.trim()).filter((message) => message !== "");
333
+ }
334
+ function claimsOnly(messages) {
335
+ return messages.length > 0 && messages.every((message) => parseClaimMessage(message).kind === "ownership");
336
+ }
337
+ function hasStartedBranch(sources, remote, refs, stub, note) {
338
+ if (!hasBranch(refs, stub, sources.branchTemplate))
339
+ return false;
340
+ const tips = readBranchTips(sources, remote, stub, note);
341
+ if (tips === null)
342
+ return true;
343
+ if (tips.length === 0) {
344
+ note(`\`${planBranchOf(sources, stub)}\` is on ${remote} but not fetched here, so its commits were not read and its plan reads as started`);
345
+ return true;
346
+ }
347
+ return tips.some((ref) => {
348
+ const messages = readBranchMessages(sources, ref, note);
349
+ return messages === null || !claimsOnly(messages);
350
+ });
351
+ }
352
+ function openWorld(sources) {
353
+ let problems = [];
354
+ const note = (problem) => {
355
+ problems = [...problems, problem];
356
+ };
357
+ const remote = sources.remote ?? REMOTE;
358
+ const runs = once(() => sources.runs());
359
+ const branch = once(() => readBranch(sources, note));
360
+ const plans = once(() => readPlans(sources, note));
361
+ const refs = once(() => readRefs(sources, remote, note));
362
+ const roadmap = once(async () => {
363
+ const reading = await sources.board.next();
364
+ reading.problems.forEach((problem) => note(problem));
365
+ return reading;
366
+ });
367
+ const checkout = once(() => readCheckout(sources, note));
368
+ return {
369
+ sources,
370
+ remote,
371
+ branch,
372
+ liveRun: once(() => runs().find((record) => isLive(record)) ?? null),
373
+ liveRunHoldsTree: once(() => holdsTree(runs().filter((record) => isLive(record)), checkout)),
374
+ standing: once(() => readStanding(sources, remote, note)),
375
+ tracked: once(() => readTracked(sources, note)),
376
+ checkout,
377
+ branchPlan: once(() => readBranchPlan(branch(), plans(), sources.branchTemplate)),
378
+ unstartedPlan: once(() => plans().find((plan) => !hasRun(runs(), plan.stub) && !hasStartedBranch(sources, remote, refs(), plan.stub, note)) ?? null),
379
+ openPull: once(() => readOpenPull(sources, branch(), note)),
380
+ roadmap,
381
+ picked: once(async () => {
382
+ if (branch() !== sources.base)
383
+ return null;
384
+ const { line } = await roadmap();
385
+ if (line === null)
386
+ return null;
387
+ return {
388
+ line,
389
+ ready: await sources.board.isReady(line.issue),
390
+ blocked: await sources.board.blocking(line.issue)
391
+ };
392
+ }),
393
+ problems: () => problems
394
+ };
395
+ }
396
+ function onBase(world) {
397
+ return world.branch() === world.sources.base;
398
+ }
399
+ function branchLabel(world) {
400
+ const branch = world.branch();
401
+ return branch === "" ? "this checkout" : `\`${branch}\``;
402
+ }
403
+
404
+ // src/next/hop-rows.ts
405
+ function oneLine(text) {
406
+ return text.replace(/\s+/g, " ").trim();
407
+ }
408
+ function ref(issue) {
409
+ return `#${String(issue)}`;
410
+ }
411
+ function placeText(place) {
412
+ return place.epic === null ? `board ${ref(place.board)}` : `epic ${ref(place.epic)} on board ${ref(place.board)}`;
413
+ }
414
+ function dryHopSentence(dry, next) {
415
+ return `hop from epic ${ref(dry.number)}: it ran dry, next \`now\` epic ${ref(next.number)} on board ${ref(next.board)}`;
416
+ }
417
+ async function hopView(world) {
418
+ if (!onBase(world))
419
+ return null;
420
+ const reading = await world.roadmap();
421
+ return reading.hop === undefined ? null : { reading, hop: reading.hop };
422
+ }
423
+ function awayRecord(hop) {
424
+ return hop.record?.state === "away" ? hop.record : null;
425
+ }
426
+ function homeOf(view) {
427
+ return view.hop.position?.home ?? { board: view.reading.roadmap, epic: null };
428
+ }
429
+ function homeAnswer(id, reading, step, issue) {
430
+ return {
431
+ id,
432
+ action: "home",
433
+ reading,
434
+ proposal: `go back home to ${placeText(step.home)}`,
435
+ ...issue === null ? {} : { issue },
436
+ ...step.pullRequest === null ? {} : { pullRequest: step.pullRequest },
437
+ hop: Object.freeze(step)
438
+ };
439
+ }
440
+ async function readGate(roadmap, pullRequest) {
441
+ try {
442
+ return await roadmap.ownerApproval(pullRequest);
443
+ } catch (error) {
444
+ return {
445
+ state: "unknown",
446
+ reason: `could not read the owner gate of ${ref(pullRequest)}: ${oneLine(messageOf(error))}`,
447
+ owners: []
448
+ };
449
+ }
450
+ }
451
+ async function waitingIssue(world, pullRequest) {
452
+ let hop;
453
+ try {
454
+ hop = (await world.roadmap()).hop;
455
+ } catch {
456
+ return null;
457
+ }
458
+ const record = hop?.record ?? null;
459
+ if (record?.target === null || record === null)
460
+ return null;
461
+ const named = record.pullRequest === pullRequest || hop?.target?.pullRequest === pullRequest;
462
+ return named ? record.target : null;
463
+ }
464
+ async function readPrOwnerReview(world) {
465
+ const { roadmap } = world.sources;
466
+ if (roadmap === undefined)
467
+ return null;
468
+ const open = await world.openPull();
469
+ if (open === null || open.mergeable !== "mergeable")
470
+ return null;
471
+ if (open.verdict !== "green" && open.verdict !== "none")
472
+ return null;
473
+ const pullRequest = open.summary.number;
474
+ const gate = await readGate(roadmap, pullRequest);
475
+ if (gate.state === "not-gated" || gate.state === "approved")
476
+ return null;
477
+ const issue = await waitingIssue(world, pullRequest);
478
+ return {
479
+ id: "pr-owner-review",
480
+ action: "none",
481
+ reading: `waiting on ${ref(issue ?? pullRequest)} (owner review)`,
482
+ proposal: `leave ${ref(pullRequest)} to its owner's review, which rafa does not merge past: ${oneLine(gate.reason)}`,
483
+ pullRequest,
484
+ ...issue === null ? {} : { issue }
485
+ };
486
+ }
487
+ function dryEnded(view, record) {
488
+ const { dryEpic, line } = view.reading;
489
+ if (line !== null || dryEpic === undefined)
490
+ return null;
491
+ const step = { action: "home", home: record.home, closing: "halted", pullRequest: null };
492
+ const reading = `epic ${ref(dryEpic.number)}, where the dry hop went, has no line left, and a second hop would leave home behind`;
493
+ return homeAnswer("away-ended", reading, step, null);
494
+ }
495
+ async function notReadyEnded(world, record, target) {
496
+ const picked = await world.picked();
497
+ if (picked === null || picked.line.issue !== target || picked.ready || picked.blocked !== null)
498
+ return null;
499
+ const step = { action: "home", home: record.home, closing: "halted", pullRequest: null };
500
+ const reading = `${ref(target)}, the hop's target in epic ${ref(record.targetEpic)}, carries no \`${SPEC_READY_LABEL}\`, so its spec is refused as not ready`;
501
+ return homeAnswer("away-ended", reading, step, target);
502
+ }
503
+ async function readAwayEnded(world) {
504
+ const view = await hopView(world);
505
+ const record = view === null ? null : awayRecord(view.hop);
506
+ if (view === null || record === null)
507
+ return null;
508
+ if (record.kind === "dry")
509
+ return dryEnded(view, record);
510
+ const target = view.hop.target;
511
+ if (target === null)
512
+ return null;
513
+ const where = `${ref(target.issue)}, the hop's target in epic ${ref(record.targetEpic)}`;
514
+ if (target.closed) {
515
+ const step = { action: "home", home: record.home, closing: "merged", pullRequest: null };
516
+ return homeAnswer("away-ended", `${where}, is closed`, step, target.issue);
517
+ }
518
+ if (target.pullRequest !== null) {
519
+ const step = { action: "home", home: record.home, closing: "waiting", pullRequest: target.pullRequest };
520
+ return homeAnswer("away-ended", `${where}, has pull request ${ref(target.pullRequest)} open`, step, target.issue);
521
+ }
522
+ return notReadyEnded(world, record, target.issue);
523
+ }
524
+ function decisionOf(view, kind) {
525
+ const decision = view?.hop.decision ?? null;
526
+ return decision?.kind === kind ? decision : null;
527
+ }
528
+ async function readHopHalt(world) {
529
+ const view = await hopView(world);
530
+ const halt = decisionOf(view, "halt");
531
+ if (view === null || halt === null)
532
+ return null;
533
+ const home = view.hop.record?.home ?? view.hop.position?.home ?? halt.from;
534
+ const step = { action: "home", home, closing: "halted", pullRequest: null };
535
+ return homeAnswer("hop-halt", hopDecisionSentence(halt), step, halt.chain.blocked);
536
+ }
537
+ async function readHopBlocked(world) {
538
+ const view = await hopView(world);
539
+ const move = decisionOf(view, "hop");
540
+ if (view === null || move === null || awayRecord(view.hop) !== null)
541
+ return null;
542
+ const opening = {
543
+ kind: "blocker",
544
+ home: homeOf(view),
545
+ from: move.from,
546
+ blocked: move.blocked,
547
+ target: move.blocker,
548
+ targetEpic: move.epic,
549
+ targetBoard: move.board
550
+ };
551
+ return {
552
+ id: "hop-blocked",
553
+ action: "hop",
554
+ reading: hopDecisionSentence(move),
555
+ proposal: `hop to ${placeText(move.to)} and work ${ref(move.blocker)}, keeping home`,
556
+ issue: move.blocker,
557
+ hop: Object.freeze({ action: "hop", opening: Object.freeze(opening) })
558
+ };
559
+ }
560
+ function dryPlaces(view, dry) {
561
+ const from = { board: view.reading.roadmap, epic: dry.number };
562
+ const { position } = view.hop;
563
+ return {
564
+ from,
565
+ home: position === null || samePlace(position.current, position.home) ? from : position.home
566
+ };
567
+ }
568
+ async function readHopDry(world) {
569
+ const view = await hopView(world);
570
+ if (view === null || awayRecord(view.hop) !== null)
571
+ return null;
572
+ const { dryEpic, line } = view.reading;
573
+ const next = view.hop.nextEpic;
574
+ if (line !== null || dryEpic === undefined || next === null)
575
+ return null;
576
+ const { from, home } = dryPlaces(view, dryEpic);
577
+ const to = { board: next.board, epic: next.number };
578
+ if (!oneHopAllows(from, to, home))
579
+ return null;
580
+ const opening = {
581
+ kind: "dry",
582
+ home: homeOf(view),
583
+ from,
584
+ blocked: null,
585
+ target: null,
586
+ targetEpic: next.number,
587
+ targetBoard: next.board
588
+ };
589
+ return {
590
+ id: "hop-dry",
591
+ action: "hop",
592
+ reading: dryHopSentence(dryEpic, next),
593
+ proposal: `hop to ${placeText(to)}, keeping home`,
594
+ hop: Object.freeze({ action: "hop", opening: Object.freeze(opening) })
595
+ };
596
+ }
597
+
598
+ // src/next/preconditions.ts
599
+ var MAX_NAMED_FILES = 5;
600
+ function nameFiles(paths) {
601
+ const shown = paths.slice(0, MAX_NAMED_FILES).map((path) => `\`${path}\``);
602
+ const hidden = paths.length - shown.length;
603
+ return hidden > 0 ? `${shown.join(", ")} and ${hidden} more` : shown.join(", ");
604
+ }
605
+ function oneLine2(text) {
606
+ return text.replace(/\s+/g, " ").trim();
607
+ }
608
+ function treeLabel(checkout) {
609
+ return checkout === null ? "the working tree" : `the working tree at \`${checkout}\``;
610
+ }
611
+ function readTreeModified(world) {
612
+ const paths = world.tracked();
613
+ if (paths.length === 0)
614
+ return null;
615
+ const tree = treeLabel(world.checkout());
616
+ return {
617
+ id: "tree-modified",
618
+ action: "none",
619
+ reading: `${tree} has changes to ${plural(paths.length, "tracked file")}: ${nameFiles(paths)}`,
620
+ proposal: "commit or set aside your changes; rafa will not touch them"
621
+ };
622
+ }
623
+ async function readPullsUnusable(world) {
624
+ try {
625
+ await world.openPull();
626
+ return null;
627
+ } catch (error) {
628
+ return {
629
+ id: "pulls-unusable",
630
+ action: "none",
631
+ reading: `the \`${world.sources.pulls.kind}\` pull request provider could not be asked: ${oneLine2(messageOf(error))}`,
632
+ proposal: "run `rafa doctor` to see what the provider needs, then read the state again"
633
+ };
634
+ }
635
+ }
636
+
637
+ // src/next/state.ts
638
+ var PREFIX = "rafa next";
639
+ var NEXT_PRECONDITIONS = Object.freeze([
640
+ "tree-modified",
641
+ "pulls-unusable"
642
+ ]);
643
+ function prLabel(open) {
644
+ return `#${open.summary.number}`;
645
+ }
646
+ function readLoopRunning(world) {
647
+ const record = world.liveRun();
648
+ if (record === null)
649
+ return null;
650
+ return {
651
+ id: "loop-running",
652
+ action: "none",
653
+ reading: `a loop for \`${planLabel(record)}\` is ${record.state} on \`${record.branch}\`, session ${record.sessionId}`,
654
+ proposal: "nothing to start while it runs; `rafa loop status` says where it is",
655
+ planStub: record.planStub
656
+ };
657
+ }
658
+ function readBaseBehind(world) {
659
+ if (!onBase(world))
660
+ return null;
661
+ const standing = world.standing();
662
+ if (standing === null || standing.behind === 0)
663
+ return null;
664
+ const { base } = world.sources;
665
+ const tracking = `${world.remote}/${base}`;
666
+ const ahead = hasDiverged(standing) ? ` and ${plural(standing.ahead, "commit")} ahead of it` : "";
667
+ return {
668
+ id: "base-behind",
669
+ action: "sync",
670
+ reading: `\`${base}\` is ${plural(standing.behind, "commit")} behind \`${tracking}\`${ahead}`,
671
+ proposal: `fast-forward \`${base}\` to \`${tracking}\``
672
+ };
673
+ }
674
+ function readTrackerBlocked(world) {
675
+ const plan = world.branchPlan();
676
+ if (plan === null || plan.tasks.blocked === 0)
677
+ return null;
678
+ const held = `${plural(plan.tasks.blocked, "blocked task")} and ${plural(plan.tasks.open, "open one")}`;
679
+ return {
680
+ id: "tracker-blocked",
681
+ action: "resume",
682
+ reading: `\`${plan.stub}\` has ${held} on ${branchLabel(world)}`,
683
+ proposal: `resume the loop on \`${plan.stub}\`, which retries a blocked task first`,
684
+ planStub: plan.stub,
685
+ planPath: plan.plan
686
+ };
687
+ }
688
+ function readTrackerOpen(world) {
689
+ const plan = world.branchPlan();
690
+ if (plan === null || plan.tasks.open === 0)
691
+ return null;
692
+ return {
693
+ id: "tracker-open",
694
+ action: "resume",
695
+ reading: `\`${plan.stub}\` has ${plural(plan.tasks.open, "open task")} left on ${branchLabel(world)}`,
696
+ proposal: `resume the loop on \`${plan.stub}\``,
697
+ planStub: plan.stub,
698
+ planPath: plan.plan
699
+ };
700
+ }
701
+ async function readPrPending(world) {
702
+ const open = await world.openPull();
703
+ if (open === null)
704
+ return null;
705
+ if (open.verdict !== "pending" && open.mergeable !== "unknown")
706
+ return null;
707
+ const settling = open.verdict === "pending" ? "its checks are still running" : `GitHub has not settled whether it merges into \`${open.summary.baseRefName}\``;
708
+ return {
709
+ id: "pr-pending",
710
+ action: "wait",
711
+ reading: `${prLabel(open)} is open on \`${open.summary.headRefName}\` and ${settling}`,
712
+ proposal: `wait for the checks on ${prLabel(open)}`,
713
+ pullRequest: open.summary.number
714
+ };
715
+ }
716
+ function redClause(open) {
717
+ if (open.mergeable === "conflicting")
718
+ return `conflicts with \`${open.summary.baseRefName}\``;
719
+ return open.verdict === "red" ? "its checks are red" : null;
720
+ }
721
+ async function readPrRed(world) {
722
+ const open = await world.openPull();
723
+ if (open === null)
724
+ return null;
725
+ const why = redClause(open);
726
+ if (why === null)
727
+ return null;
728
+ return {
729
+ id: "pr-red",
730
+ action: "triage",
731
+ reading: `${prLabel(open)} is open on \`${open.summary.headRefName}\` and ${why}`,
732
+ proposal: `triage ${prLabel(open)}`,
733
+ pullRequest: open.summary.number
734
+ };
735
+ }
736
+ async function readPrNoChecks(world) {
737
+ const open = await world.openPull();
738
+ if (open === null || open.verdict !== "none" || open.mergeable !== "mergeable")
739
+ return null;
740
+ const base = `\`${open.summary.baseRefName}\``;
741
+ return {
742
+ id: "pr-no-checks",
743
+ action: "merge-unchecked",
744
+ reading: `${prLabel(open)} is open on \`${open.summary.headRefName}\`, reports no check at all and merges into ${base}`,
745
+ proposal: `merge ${prLabel(open)} into ${base} with no checks`,
746
+ pullRequest: open.summary.number
747
+ };
748
+ }
749
+ async function readPrGreen(world) {
750
+ const open = await world.openPull();
751
+ if (open === null || open.verdict !== "green" || open.mergeable !== "mergeable")
752
+ return null;
753
+ const base = `\`${open.summary.baseRefName}\``;
754
+ return {
755
+ id: "pr-green",
756
+ action: "merge",
757
+ reading: `${prLabel(open)} is open on \`${open.summary.headRefName}\`, green and merges into ${base}`,
758
+ proposal: `merge ${prLabel(open)} into ${base}`,
759
+ pullRequest: open.summary.number
760
+ };
761
+ }
762
+ function readPlanUnstarted(world) {
763
+ if (!onBase(world))
764
+ return null;
765
+ const plan = world.unstartedPlan();
766
+ if (plan === null)
767
+ return null;
768
+ return {
769
+ id: "plan-unstarted",
770
+ action: "start",
771
+ reading: `\`${plan.stub}\` is planned, with no run and no branch`,
772
+ proposal: `start the loop on \`${plan.stub}\`, creating its branch`,
773
+ planStub: plan.stub,
774
+ planPath: plan.plan
775
+ };
776
+ }
777
+ async function readIssueReady(world) {
778
+ const picked = await world.picked();
779
+ if (picked === null || !picked.ready || picked.blocked !== null)
780
+ return null;
781
+ const { issue } = picked.line;
782
+ return {
783
+ id: "issue-ready",
784
+ action: "plan",
785
+ reading: `#${issue} is next on the roadmap and carries \`${SPEC_READY_LABEL}\``,
786
+ proposal: `create the plan for #${issue}`,
787
+ issue
788
+ };
789
+ }
790
+ async function readIssueBlocked(world) {
791
+ if (world.sources.board.mode === "native")
792
+ return null;
793
+ const picked = await world.picked();
794
+ if (picked === null || picked.blocked === null)
795
+ return null;
796
+ const { issue } = picked.line;
797
+ return {
798
+ id: "issue-blocked",
799
+ action: "unblock",
800
+ reading: blockedLineSentence(picked.blocked),
801
+ proposal: `re-read the blockers of #${issue} and take \`${SPEC_BLOCKED_LABEL}\` off once they have all closed`,
802
+ issue
803
+ };
804
+ }
805
+ async function readIssueNotReady(world) {
806
+ const picked = await world.picked();
807
+ if (picked === null || picked.blocked !== null)
808
+ return null;
809
+ const { issue } = picked.line;
810
+ return {
811
+ id: "issue-not-ready",
812
+ action: "ready",
813
+ reading: notReadySentence(issue),
814
+ proposal: `check the spec of #${issue} and mark it ready`,
815
+ issue
816
+ };
817
+ }
818
+ function readNothingHere(world) {
819
+ const where = branchLabel(world);
820
+ const plan = world.branchPlan();
821
+ return {
822
+ id: "nothing-left",
823
+ action: "none",
824
+ reading: plan === null ? `${where} is no plan branch, and it has no open pull request` : `\`${plan.stub}\` has no task left on ${where}, and it has no open pull request`,
825
+ proposal: `nothing to run from ${where}; the cycle goes on from \`${world.sources.base}\``,
826
+ planStub: plan?.stub ?? null
827
+ };
828
+ }
829
+ async function readNothingLeft(world) {
830
+ if (!onBase(world))
831
+ return readNothingHere(world);
832
+ const roadmap = await world.roadmap();
833
+ const passed = plural(roadmap.passed, "line");
834
+ return {
835
+ id: "nothing-left",
836
+ action: "none",
837
+ reading: `the roadmap, issue #${roadmap.roadmap}, has no line left that is not done or taken (${passed} passed)`,
838
+ proposal: "open the next spec issue and add it to the roadmap"
839
+ };
840
+ }
841
+ var ROWS = Object.freeze([
842
+ { id: "loop-running", read: readLoopRunning },
843
+ { id: "base-behind", read: readBaseBehind },
844
+ { id: "tracker-blocked", read: readTrackerBlocked },
845
+ { id: "tracker-open", read: readTrackerOpen },
846
+ { id: "pr-pending", read: readPrPending, pulls: true },
847
+ { id: "pr-red", read: readPrRed, pulls: true },
848
+ { id: "pr-no-checks", read: readPrNoChecks, pulls: true },
849
+ { id: "pr-green", read: readPrGreen, pulls: true },
850
+ { id: "plan-unstarted", read: readPlanUnstarted },
851
+ { id: "issue-ready", read: readIssueReady },
852
+ { id: "issue-blocked", read: readIssueBlocked },
853
+ { id: "issue-not-ready", read: readIssueNotReady },
854
+ { id: "nothing-left", read: readNothingLeft }
855
+ ]);
856
+ var NEXT_STATES = Object.freeze(ROWS.map((row) => row.id));
857
+ var HOP_ROWS_AHEAD_OF = Object.freeze({
858
+ "pr-no-checks": [{ id: "pr-owner-review", read: readPrOwnerReview, pulls: true }],
859
+ "plan-unstarted": [{ id: "away-ended", read: readAwayEnded }],
860
+ "issue-blocked": [{ id: "hop-halt", read: readHopHalt }, { id: "hop-blocked", read: readHopBlocked }],
861
+ "nothing-left": [{ id: "hop-dry", read: readHopDry }]
862
+ });
863
+ var ROADMAP_ROWS = Object.freeze(ROWS.flatMap((row) => [...HOP_ROWS_AHEAD_OF[row.id] ?? [], row]));
864
+ var NEXT_ROADMAP_STATES = Object.freeze(ROADMAP_ROWS.map((row) => row.id));
865
+ var LOOP_ROW = new Set(["loop-running"]);
866
+ var NO_ROW = new Set;
867
+ function aheadOfTree(world) {
868
+ return world.liveRunHoldsTree() ? LOOP_ROW : NO_ROW;
869
+ }
870
+ async function firstAnswer(world, rows) {
871
+ for (const row of rows) {
872
+ if (row.pulls === true) {
873
+ const unusable = await readPullsUnusable(world);
874
+ if (unusable !== null)
875
+ return unusable;
876
+ }
877
+ const found = await row.read(world);
878
+ if (found !== null)
879
+ return found;
880
+ }
881
+ return null;
882
+ }
883
+ function answer(row, problems) {
884
+ return Object.freeze({
885
+ id: row.id,
886
+ action: row.action,
887
+ reading: row.reading,
888
+ proposal: row.proposal,
889
+ pullRequest: row.pullRequest ?? null,
890
+ issue: row.issue ?? null,
891
+ planStub: row.planStub ?? null,
892
+ planPath: row.planPath ?? null,
893
+ problems: Object.freeze([...problems]),
894
+ ...row.hop === undefined ? {} : { hop: row.hop }
895
+ });
896
+ }
897
+ async function readNextState(sources) {
898
+ const world = openWorld(sources);
899
+ const rows = sources.roadmap === undefined ? ROWS : ROADMAP_ROWS;
900
+ const ahead = aheadOfTree(world);
901
+ const found = await firstAnswer(world, rows.filter((row) => ahead.has(row.id))) ?? readTreeModified(world) ?? await firstAnswer(world, rows.filter((row) => !ahead.has(row.id)));
902
+ if (found !== null)
903
+ return answer(found, world.problems());
904
+ throw new Error(`${PREFIX}: no row of the table answered, and the last row answers for every reading`);
905
+ }
906
+
907
+ // src/next/hint.ts
908
+ var HINT_FLAG = "hint";
909
+ var HINT_TIMEOUT_MS = 2000;
910
+ var HINT_MARK = "\uD83D\uDC49";
911
+ var HINT_LEAD = "Next:";
912
+ function commandWords(invocation) {
913
+ return [invocation.command, ...invocation.argv].join(" ");
914
+ }
915
+ function capitalised(text) {
916
+ return `${text.slice(0, 1).toUpperCase()}${text.slice(1)}`;
917
+ }
918
+ function nextQuestion(state) {
919
+ return `${capitalised(state.proposal)}? [y/N] `;
920
+ }
921
+ function hintLine(state, invocation) {
922
+ return `${HINT_MARK} ${HINT_LEAD} ${state.proposal} \u2014 rafa ${commandWords(invocation)}`;
923
+ }
924
+ function wantsHint(flags) {
925
+ const value = flags[HINT_FLAG];
926
+ return value !== false && value !== "false";
927
+ }
928
+ var DEFAULT_HINT_SEAMS = Object.freeze({});
929
+ function afterDelay(ms) {
930
+ return new Promise((resolve) => {
931
+ setTimeout(resolve, ms).unref();
932
+ });
933
+ }
934
+ function silenced() {
935
+ return Object.freeze({
936
+ info: () => {
937
+ return;
938
+ },
939
+ warn: () => {
940
+ return;
941
+ },
942
+ error: () => {
943
+ return;
944
+ },
945
+ debug: () => {
946
+ return;
947
+ },
948
+ emit: () => {
949
+ return;
950
+ },
951
+ result: () => {
952
+ return;
953
+ }
954
+ });
955
+ }
956
+ async function readOnce(context, seams) {
957
+ const relations = await openNextRelations(context, seams);
958
+ const sources = await openNextSources({ ...context, output: silenced() }, seams, relations === undefined ? {} : { relations });
959
+ return readNextState(sources);
960
+ }
961
+ async function readWithin(read, expire) {
962
+ const reading = Promise.resolve().then(read).then((state) => ({ state }), () => null);
963
+ const raced = await Promise.race([reading, expire(HINT_TIMEOUT_MS).then(() => null)]);
964
+ return raced === null ? null : raced.state;
965
+ }
966
+ async function nextStepHint(context, seams = DEFAULT_HINT_SEAMS) {
967
+ if (!wantsHint(context.flags))
968
+ return null;
969
+ try {
970
+ const read = seams.readState ?? (() => readOnce(context, seams));
971
+ const state = await readWithin(read, seams.expire ?? afterDelay);
972
+ if (state === null)
973
+ return null;
974
+ const invocation = actionInvocation(state);
975
+ if (invocation === null)
976
+ return null;
977
+ const isTerminal = seams.isTerminal ?? (() => process.stdin.isTTY === true);
978
+ if (isTerminal()) {
979
+ return Object.freeze({ kind: "question", state, invocation, line: nextQuestion(state) });
980
+ }
981
+ const line = hintLine(state, invocation);
982
+ context.output.info(line);
983
+ return Object.freeze({ kind: "command", state, invocation, line });
984
+ } catch {
985
+ return null;
986
+ }
987
+ }
988
+
989
+ // src/next/hop-action.ts
990
+ import { rmSync } from "fs";
991
+ var NO_REHOME = "--no-rehome";
992
+ function hopLogLine(opening) {
993
+ const { from, blocked, target, targetEpic, targetBoard } = opening;
994
+ if (opening.kind === "dry" || blocked === null || target === null) {
995
+ return dryHopSentence({ number: from.epic ?? from.board }, { number: targetEpic, board: targetBoard });
996
+ }
997
+ return hopMoveSentence({ from, blocked, blocker: target, epic: targetEpic });
998
+ }
999
+ function homeLogLine(home) {
1000
+ return `back home: ${placeText(home)}`;
1001
+ }
1002
+ function openedRecord(opening, now) {
1003
+ return {
1004
+ ...opening,
1005
+ state: "away",
1006
+ pullRequest: null,
1007
+ startedAt: now.toISOString()
1008
+ };
1009
+ }
1010
+ function writeRecord(root, record) {
1011
+ try {
1012
+ writeHopRecord(root, record);
1013
+ } catch (error) {
1014
+ throw new CommandExit(1, `\u274C Could not write ${hopFilePath(root)}: ${messageOf(error)}`);
1015
+ }
1016
+ }
1017
+ function restoreRecord(root, before) {
1018
+ if (before.set)
1019
+ writeHopRecord(root, before.record);
1020
+ else
1021
+ rmSync(hopFilePath(root), { force: true });
1022
+ }
1023
+ async function runHop(world, step) {
1024
+ const { opening } = step;
1025
+ const before = readHopRecord(world.root);
1026
+ const record = openedRecord(opening, world.now());
1027
+ writeRecord(world.root, record);
1028
+ try {
1029
+ await world.switchTo([String(opening.targetEpic), NO_REHOME]);
1030
+ } catch (error) {
1031
+ restoreRecord(world.root, before);
1032
+ throw error;
1033
+ }
1034
+ world.info(hopLogLine(opening));
1035
+ return record;
1036
+ }
1037
+ function writePosition(root, position) {
1038
+ try {
1039
+ writePositionFile(root, position);
1040
+ } catch (error) {
1041
+ throw new CommandExit(1, `\u274C Could not write ${positionFilePath(root)}: ${messageOf(error)}`);
1042
+ }
1043
+ }
1044
+ function movedHome(world, step) {
1045
+ const reading = readPositionFile(world.root);
1046
+ if (reading.set) {
1047
+ if (samePlace(reading.position.current, reading.position.home))
1048
+ return { now: reading.position, written: false };
1049
+ return { now: goHome(reading.position), written: true };
1050
+ }
1051
+ if (reading.reason === "absent")
1052
+ return { now: null, written: false };
1053
+ world.warn(`${reading.detail}, so it is written afresh at home`);
1054
+ return { now: positionAt(step.home), written: true };
1055
+ }
1056
+ function awayToClose(world, position) {
1057
+ const reading = readHopRecord(world.root);
1058
+ if (!reading.set) {
1059
+ if (reading.reason !== "absent")
1060
+ world.warn(`${reading.detail}, so no hop record is closed`);
1061
+ return null;
1062
+ }
1063
+ const { record } = reading;
1064
+ if (record.state !== "away" || position === null)
1065
+ return null;
1066
+ return staleAgainst(record, position) ? null : record;
1067
+ }
1068
+ function runHome(world, step) {
1069
+ const moved = movedHome(world, step);
1070
+ if (moved.written && moved.now !== null)
1071
+ writePosition(world.root, moved.now);
1072
+ const away = awayToClose(world, moved.now);
1073
+ const closed = away === null ? null : { ...away, state: step.closing, pullRequest: step.pullRequest ?? away.pullRequest };
1074
+ if (closed !== null)
1075
+ writeRecord(world.root, closed);
1076
+ const line = homeLogLine(moved.now?.home ?? step.home);
1077
+ world.info(line);
1078
+ return Object.freeze({
1079
+ position: moved.written ? moved.now : null,
1080
+ record: closed,
1081
+ line
1082
+ });
1083
+ }
1084
+
1085
+ // src/next/actions.ts
1086
+ var PREFIX2 = "rafa next";
1087
+ var NEXT_IN_PROCESS_ACTIONS = Object.freeze(["sync", "hop", "home"]);
1088
+ var SWITCH_COMMAND = Object.freeze({ subject: "switch", action: "switch" });
1089
+ var ROADMAP_WORD = "--roadmap";
1090
+ var ROADMAP_PASSED_ACTIONS = new Set(["plan", "start", "resume"]);
1091
+ var CLAIM_AHEAD_WORD = "--claim-ahead";
1092
+ function passedWords(action, options) {
1093
+ if (options.roadmap !== true || !ROADMAP_PASSED_ACTIONS.has(action))
1094
+ return [];
1095
+ return options.claimAhead === true && action === "plan" ? [ROADMAP_WORD, CLAIM_AHEAD_WORD] : [ROADMAP_WORD];
1096
+ }
1097
+ function needed(state, value, what) {
1098
+ if (value === null) {
1099
+ throw new Error(`${PREFIX2}: state "${state.id}" proposes "${state.action}" and names no ${what}`);
1100
+ }
1101
+ return String(value);
1102
+ }
1103
+ function pullRequestOf(state) {
1104
+ return needed(state, state.pullRequest, "pull request");
1105
+ }
1106
+ function issueOf(state) {
1107
+ return needed(state, state.issue, "issue");
1108
+ }
1109
+ function planFlagOf(state) {
1110
+ return `--plan=${needed(state, state.planPath, "plan file")}`;
1111
+ }
1112
+ var ACTION_COMMANDS = Object.freeze({
1113
+ resume: { subject: "loop", action: "start", argv: (state) => [planFlagOf(state)] },
1114
+ wait: { subject: "pr", action: "wait", argv: (state) => [pullRequestOf(state)] },
1115
+ triage: { subject: "pr", action: "triage", argv: (state) => [pullRequestOf(state)] },
1116
+ merge: { subject: "pr", action: "merge", argv: (state) => [pullRequestOf(state), "--yes"] },
1117
+ "merge-unchecked": { subject: "pr", action: "merge", argv: (state) => [pullRequestOf(state), "--skip-checks"] },
1118
+ settle: { subject: "release", action: "settle", argv: () => [] },
1119
+ start: { subject: "loop", action: "start", argv: (state) => [planFlagOf(state), "--create-branch"] },
1120
+ plan: { subject: "plan", action: "create", argv: () => ["--next"] },
1121
+ unblock: { subject: "issue", action: "unblock", argv: (state) => [issueOf(state)] },
1122
+ ready: { subject: "issue", action: "ready", argv: (state) => [issueOf(state)] }
1123
+ });
1124
+ var NEXT_COMMAND_ACTIONS = Object.freeze(Object.keys(ACTION_COMMANDS));
1125
+ var LABELS_ONLY_ACTIONS = new Set(["unblock"]);
1126
+ var OFFERED_COMMAND_ACTIONS = Object.freeze({
1127
+ labels: NEXT_COMMAND_ACTIONS,
1128
+ native: Object.freeze(NEXT_COMMAND_ACTIONS.filter((action) => !LABELS_ONLY_ACTIONS.has(action)))
1129
+ });
1130
+ function offeredCommandActions(mode) {
1131
+ return OFFERED_COMMAND_ACTIONS[mode];
1132
+ }
1133
+ function notOffered(state, mode) {
1134
+ return new Error(`${PREFIX2}: state "${state.id}" proposes "${state.action}", which is offered in labels mode only` + ` and board.relationships is ${mode}: the tracker clears a blocker when it closes`);
1135
+ }
1136
+ function runsCommand(action) {
1137
+ return NEXT_COMMAND_ACTIONS.includes(action);
1138
+ }
1139
+ function actionInvocation(state, options = {}) {
1140
+ if (!runsCommand(state.action))
1141
+ return null;
1142
+ const mode = options.mode ?? "labels";
1143
+ if (!offeredCommandActions(mode).includes(state.action))
1144
+ throw notOffered(state, mode);
1145
+ const spec = ACTION_COMMANDS[state.action];
1146
+ return Object.freeze({
1147
+ action: state.action,
1148
+ command: commandSpelling(spec),
1149
+ argv: Object.freeze([...spec.argv(state), ...passedWords(state.action, options)])
1150
+ });
1151
+ }
1152
+ function actionContext(caller, command, argv) {
1153
+ const { positional, flags } = parseArgs(argv, command);
1154
+ return Object.freeze({
1155
+ ...caller,
1156
+ args: Object.freeze(positional),
1157
+ flags: Object.freeze({ ...flags, [HINT_FLAG]: false }),
1158
+ argv: Object.freeze([...argv])
1159
+ });
1160
+ }
1161
+ async function runRegistered(caller, name, argv, step) {
1162
+ const command = caller.registry.find(name.subject, name.action);
1163
+ if (command === undefined) {
1164
+ throw new CommandExit(1, `\u274C ${PREFIX2}: the "${step}" step runs "rafa ${commandSpelling(name)}", which is registered by nothing`);
1165
+ }
1166
+ await command.run(actionContext(caller, command, argv));
1167
+ }
1168
+ function hopWorld(caller, state) {
1169
+ if (caller.project === null) {
1170
+ throw new Error(`${PREFIX2}: the action "${state.action}" of state "${state.id}" runs inside a project, and was handed none`);
1171
+ }
1172
+ return {
1173
+ root: caller.project.root,
1174
+ switchTo: (argv) => runRegistered(caller, SWITCH_COMMAND, argv, state.action),
1175
+ info: (line) => {
1176
+ caller.output.info(line);
1177
+ },
1178
+ warn: (line) => {
1179
+ caller.output.warn(line);
1180
+ },
1181
+ now: () => new Date
1182
+ };
1183
+ }
1184
+ function noStep(state) {
1185
+ return new Error(`${PREFIX2}: state "${state.id}" proposes "${state.action}" and carries no ${state.action} step`);
1186
+ }
1187
+ async function runHopStep(caller, state) {
1188
+ const step = state.hop;
1189
+ if (step?.action !== state.action)
1190
+ throw noStep(state);
1191
+ if (step.action === "hop") {
1192
+ await runHop(hopWorld(caller, state), step);
1193
+ return;
1194
+ }
1195
+ runHome(hopWorld(caller, state), step);
1196
+ }
1197
+ async function runAction(caller, state, options = {}) {
1198
+ if (state.action === "hop" || state.action === "home") {
1199
+ await runHopStep(caller, state);
1200
+ return;
1201
+ }
1202
+ const invocation = actionInvocation(state, options);
1203
+ if (invocation === null) {
1204
+ throw new Error(`${PREFIX2}: the action "${state.action}" of state "${state.id}" runs no registered command`);
1205
+ }
1206
+ await runRegistered(caller, ACTION_COMMANDS[invocation.action], invocation.argv, invocation.action);
1207
+ }
1208
+
1209
+ // src/next/ending.ts
1210
+ var HINT_FLAG_SPEC = Object.freeze({
1211
+ name: HINT_FLAG,
1212
+ description: "Ends by naming the one step that follows, asking it where there is a terminal to answer on;" + " `--no-hint` reads nothing and prints nothing.",
1213
+ type: "boolean",
1214
+ default: true
1215
+ });
1216
+ function actionOutput(output) {
1217
+ return Object.freeze({ ...output, result: () => {
1218
+ return;
1219
+ } });
1220
+ }
1221
+ var DEFAULT_ENDING_SEAMS = Object.freeze({});
1222
+ async function endWithNextStep(context, seams = DEFAULT_ENDING_SEAMS) {
1223
+ const hint = await nextStepHint(context, seams);
1224
+ if (hint === null)
1225
+ return null;
1226
+ if (hint.kind !== "question")
1227
+ return Object.freeze({ hint, asked: false, ran: false });
1228
+ const open = seams.openPrompter ?? (() => createLinePrompter(process.stdin, process.stderr));
1229
+ const prompter = open();
1230
+ let taken;
1231
+ try {
1232
+ taken = answeredYes(await prompter.ask(hint.line));
1233
+ } finally {
1234
+ prompter.close();
1235
+ }
1236
+ if (!taken)
1237
+ return Object.freeze({ hint, asked: true, ran: false });
1238
+ await runAction({ ...context, output: actionOutput(context.output) }, hint.state);
1239
+ return Object.freeze({ hint, asked: true, ran: true });
1240
+ }
1241
+
1242
+ // src/commands/issue/declarations.ts
1243
+ var ISSUE_SUBJECT = Object.freeze({
1244
+ name: "issue",
1245
+ summary: "the tracker: list, show, create, edit, comment on and move issues; mark one ready, unblock it and check its references"
1246
+ });
1247
+ var ISSUE_STATE_WORDS = Object.freeze([
1248
+ "backlog",
1249
+ "todo",
1250
+ "in-progress",
1251
+ "in-review",
1252
+ "done",
1253
+ "released",
1254
+ "cancelled"
1255
+ ]);
1256
+ var ISSUE_TYPE_WORDS = Object.freeze([
1257
+ "code",
1258
+ "bug",
1259
+ "spike",
1260
+ "adr",
1261
+ "chore",
1262
+ "package-api",
1263
+ "epic",
1264
+ "spec"
1265
+ ]);
1266
+ var ISSUE_PRIORITY_WORDS = Object.freeze([
1267
+ "urgent",
1268
+ "high",
1269
+ "medium",
1270
+ "low"
1271
+ ]);
1272
+ var ISSUE_TRIAGE_MODULE = "unassigned";
1273
+ var ISSUE_READY_LABEL = "spec:ready";
1274
+ var ISSUE_NEEDS_WORK_LABEL = "spec:needs-work";
1275
+ var ISSUE_BLOCKED_LABEL = "spec:blocked";
1276
+ var ISSUE_HINT_FLAG_SPEC = Object.freeze({
1277
+ name: "hint",
1278
+ description: "Ends by naming the one step that follows, asking it where there is a terminal to answer on;" + " `--no-hint` reads nothing and prints nothing.",
1279
+ type: "boolean",
1280
+ default: true
1281
+ });
1282
+ var DEFAULT_ISSUE_TYPE = "code";
1283
+ var PRIORITY_FLAG = "priority";
1284
+ var STAMP_FLAG = "stamp";
1285
+ var READY_YES_FLAG = "yes";
1286
+ var READY_DRY_RUN_FLAG = "dry-run";
1287
+ var UNBLOCK_ALL_FLAG = "all";
1288
+ var UNBLOCK_YES_FLAG = "yes";
1289
+ var ISSUE_LIST_FLAGS = Object.freeze([
1290
+ {
1291
+ name: "roadmap",
1292
+ description: "List the unticked lines of the Roadmap issue instead, in its order, with the spec, blocked by," + " has and refs columns, and the epics it names grouped by horizon. Reads the GitHub board whatever" + " tracker the chain lands on.",
1293
+ type: "boolean"
1294
+ },
1295
+ {
1296
+ name: "all",
1297
+ description: "With --roadmap, keep the ticked lines too, each in its place, and show the epics of every" + " horizon, not only now. Refused without --roadmap.",
1298
+ type: "boolean"
1299
+ },
1300
+ {
1301
+ name: "full",
1302
+ description: "With --roadmap, print each epic's issues under its row, on two rows each: number, state and" + " title, then labels and blockers. Refused without --roadmap.",
1303
+ type: "boolean"
1304
+ },
1305
+ {
1306
+ name: "check",
1307
+ description: "With --roadmap, exit 1 when any type:epic issue on the board is open with its work done, or" + " closed with it not done, or when the board could not be read, naming each. Refused without --roadmap.",
1308
+ type: "boolean"
1309
+ },
1310
+ {
1311
+ name: "labels",
1312
+ description: "With --roadmap, print each issue's labels on a row of their own under it, and under each --full" + " member; they are no column. Refused without --roadmap.",
1313
+ type: "boolean"
1314
+ },
1315
+ {
1316
+ name: "texts",
1317
+ description: "With --roadmap, spell the spec and blocked by columns in words instead of symbols, with no legend." + " Refused without --roadmap.",
1318
+ type: "boolean",
1319
+ aliases: ["t"]
1320
+ },
1321
+ {
1322
+ name: "refresh",
1323
+ description: "With --roadmap, read the whole board again instead of only what changed since the listing kept" + " under .rafa/cache/. Refused without --roadmap.",
1324
+ type: "boolean"
1325
+ },
1326
+ {
1327
+ name: "state",
1328
+ description: `Only issues in this state: one of ${ISSUE_STATE_WORDS.join(", ")}. Refused on the github tracker` + " and beside --roadmap.",
1329
+ type: "string"
1330
+ },
1331
+ {
1332
+ name: "type",
1333
+ description: `Only issues of this type: one of ${ISSUE_TYPE_WORDS.join(", ")}.`,
1334
+ type: "string"
1335
+ },
1336
+ {
1337
+ name: "module",
1338
+ description: "Only issues of this module.",
1339
+ type: "string"
1340
+ },
1341
+ {
1342
+ name: "search",
1343
+ description: "Only issues whose title or body holds this text.",
1344
+ type: "string"
1345
+ },
1346
+ {
1347
+ name: "limit",
1348
+ description: "At most this many issues, a positive whole number. The limit of the tracker when left out," + " and none under --roadmap.",
1349
+ type: "number"
1350
+ }
1351
+ ]);
1352
+ var ISSUE_LIST_DECLARATION = Object.freeze({
1353
+ name: "issue list",
1354
+ subject: ISSUE_SUBJECT.name,
1355
+ action: "list",
1356
+ summary: "list the issues on the tracker, or the Roadmap's lines in its order, narrowed by type, module or text",
1357
+ description: "Lists the issues on the tracker `tracker.default` names in `.rafa/config.yaml`, or on the first" + " `tracker.fallback` kind whose preflight passes when it fails, each kind passed over warned about." + " Prints the tracker, then one row per issue: its id, state, type and title; under `--type=bug`, a header" + " and the columns id, state, module, action and summary, read back from each title. Each flag narrows the" + " list, and a flag left out narrows by nothing. The github tracker holds an issue open or closed, so" + " it refuses `--state`, and lists 30 issues unless `--limit` says otherwise. With `--roadmap` it lists" + " the unticked lines of the Roadmap issue (`roadmap.issue`, else the lowest-numbered open type:roadmap" + " board, else the open issue titled Roadmap) in its" + " order instead, read off the GitHub board, as a table adding four columns: spec, whether the issue" + " can be planned (every section of the spec template filled, and the spec:ready label on it), as a" + " symbol; blocked by, the blockers grouped by whether they are still open;" + " has, a plan, a branch or a pull request already made for it; and refs, how many references of the" + " issue's saved copy under `specs.dir` read suspect or dangling, `-` with no copy. A legend names each" + " symbol, `--texts` (`-t`) spells them in words, and `--labels` prints the labels on a row under each" + " issue. A Roadmap naming epics prints them alone, grouped by horizon under a `Roadmap #<n> \xB7 <horizon>`" + " heading with the columns #, state, done/total, blocked, date and title, the now horizon only unless" + " `--all`, with one line counting the lines naming no epic; `--full` prints each epic's issues under" + " its row, and the lines naming no epic as the table. The board listing is kept under .rafa/cache/ and" + " read for what changed since; `--refresh` reads the whole board again. `--check` exits 1 when any" + " type:epic issue's stored state" + " disagrees with its computed one. `--type`, `--module`, `--search` and `--limit` then narrow the spec rows, keeping their" + " order, and an unreachable board is warned about with the rows still printed. With `--output=json`" + " the tracker, the query and every issue, or under `--roadmap` the roadmap, the rows, the epics and the" + " warnings, are the data of the terminal result event.",
1358
+ args: [],
1359
+ flags: [...ISSUE_LIST_FLAGS],
1360
+ examples: [
1361
+ {
1362
+ cmd: "rafa issue list",
1363
+ note: "Lists the issues on the tracker the chain lands on, one row each."
1364
+ },
1365
+ {
1366
+ cmd: "rafa issue list --type=bug --search=timeout",
1367
+ note: "Lists the bugs whose title or body holds the word timeout, in the columns id, state, module, action and summary."
1368
+ },
1369
+ {
1370
+ cmd: "rafa issue list --limit=5 --output=json",
1371
+ note: "Writes a start event, then a result event whose data holds the tracker, the query and five issues at most."
1372
+ },
1373
+ {
1374
+ cmd: "rafa issue list --roadmap",
1375
+ note: "Prints the Roadmap's unticked lines in its order, with the spec, blocked by, has and refs of each."
1376
+ },
1377
+ {
1378
+ cmd: "rafa issue list --roadmap --full",
1379
+ note: "Prints the Roadmap's now epics with each epic's issues underneath, then its spec lines."
1380
+ },
1381
+ {
1382
+ cmd: "rafa issue list --roadmap --check",
1383
+ note: "Prints the Roadmap, then exits 1 when an epic is open with its work done or closed with it not done."
1384
+ },
1385
+ {
1386
+ cmd: "rafa issue list --roadmap --all --type=bug",
1387
+ note: "Prints the bugs on the Roadmap, ticked lines included, in the Roadmap's order, beside its epics of every horizon."
1388
+ }
1389
+ ],
1390
+ outputs: ["text", "json"]
1391
+ });
1392
+ var ISSUE_SHOW_DECLARATION = Object.freeze({
1393
+ name: "issue show",
1394
+ subject: ISSUE_SUBJECT.name,
1395
+ action: "show",
1396
+ summary: "show one issue on the tracker: its fields, then its body",
1397
+ description: "Reads one issue on the tracker `tracker.default` names in `.rafa/config.yaml`, or on the first" + " `tracker.fallback` kind whose preflight passes when it fails, each kind passed over warned about." + " The id is the issue number on that tracker, so on a tracker fallen back to it names a different" + " issue. Prints the tracker, the id, the URL when there is one, the title, state, type, module and" + " priority, then the body. With `--output=json` the tracker and the issue are the data of the" + " terminal result event.",
1398
+ args: [
1399
+ {
1400
+ name: "id",
1401
+ description: "The issue id on the tracker: a GitHub issue number, or the number of a file under `.rafa/issues/`.",
1402
+ type: "string",
1403
+ required: true
1404
+ }
1405
+ ],
1406
+ flags: [],
1407
+ examples: [
1408
+ {
1409
+ cmd: "rafa issue show 12",
1410
+ note: "Prints issue 12 on the tracker the chain lands on, its fields and then its body."
1411
+ },
1412
+ {
1413
+ cmd: "rafa issue show 12 --output=json",
1414
+ note: "Writes a start event, then a result event whose data holds the tracker and the issue."
1415
+ }
1416
+ ],
1417
+ outputs: ["text", "json"]
1418
+ });
1419
+ var ISSUE_CREATE_DECLARATION = Object.freeze({
1420
+ name: "issue create",
1421
+ subject: ISSUE_SUBJECT.name,
1422
+ action: "create",
1423
+ summary: "create an issue on the tracker from a title and a body, a bug from its fields",
1424
+ description: "Files one issue on the tracker `tracker.default` names in `.rafa/config.yaml`, or on the first" + " `tracker.fallback` kind whose preflight passes when it fails, each kind passed over warned about." + " `--title` is required for every type but `bug`. The issue is of type `code` and module `unassigned` unless" + " `--type` and `--module` say otherwise, and with no `--priority` the tracker marks it needs-triage. A" + " `--type=bug` line is filed from its fields: `--module`, `--action`, `--summary`, `--cause` and" + " `--reproduce`, each also read from a section of the body, with rafa filling the versions and the place," + " rendering the title through `issues.titles.bug` and refusing a record missing a field, naming each. The body is" + " `--body`, or what the file `--body-file` names holds, standard input for `-`. A spec whose body" + " carries a `Blocked by: #24` line is filed blocked, and refused when the line names no issue, the" + " issue itself or an issue the board does not hold. With `board.project.number` set, a GitHub issue is" + " added to the project and its fields filled, any failure there a warning that keeps the exit code. Prints the" + " tracker and id of the issue filed, and its URL when there is one. With `--output=json` the tracker" + " and the ref are the data of the terminal result event. `--dry-run` prints the title and body the line" + " would file, resolves no tracker and files nothing.",
1425
+ args: [],
1426
+ flags: [
1427
+ {
1428
+ name: "title",
1429
+ description: "The title of the issue. Required for every type but bug, whose title is rendered from its fields.",
1430
+ type: "string"
1431
+ },
1432
+ {
1433
+ name: "body",
1434
+ description: "The body of the issue, as markdown. Empty when left out. Not with --body-file.",
1435
+ type: "string"
1436
+ },
1437
+ {
1438
+ name: "body-file",
1439
+ description: "A file holding the body of the issue, read whole; `-` reads standard input. Not with --body.",
1440
+ type: "string"
1441
+ },
1442
+ {
1443
+ name: "type",
1444
+ description: `What the issue is for: one of ${ISSUE_TYPE_WORDS.join(", ")}.`,
1445
+ type: "string",
1446
+ default: DEFAULT_ISSUE_TYPE
1447
+ },
1448
+ {
1449
+ name: "module",
1450
+ description: `The module the issue belongs to; ${ISSUE_TRIAGE_MODULE} when left out. A bug names one, or unknown.`,
1451
+ type: "string"
1452
+ },
1453
+ {
1454
+ name: "priority",
1455
+ description: `How urgent the issue is: one of ${ISSUE_PRIORITY_WORDS.join(", ")}. Marked needs-triage when left out.`,
1456
+ type: "string"
1457
+ },
1458
+ {
1459
+ name: "summary",
1460
+ description: "A bug: one line saying what is wrong, the last part of its title.",
1461
+ type: "string"
1462
+ },
1463
+ {
1464
+ name: "action",
1465
+ description: "A bug: what was running, a command, a step or a test; or unknown.",
1466
+ type: "string"
1467
+ },
1468
+ {
1469
+ name: "cause",
1470
+ description: "A bug: its cause code from `rafa bug codes`, or <family>:new-context when no leaf fits.",
1471
+ type: "string"
1472
+ },
1473
+ {
1474
+ name: "proposed",
1475
+ description: "A bug: beside a new-context cause, the leaf you would have used, as <family>:<leaf>.",
1476
+ type: "string"
1477
+ },
1478
+ {
1479
+ name: "reproduce",
1480
+ description: "A bug: the steps, or the condition that revealed it. Never unknown.",
1481
+ type: "string"
1482
+ },
1483
+ {
1484
+ name: "rerun",
1485
+ description: "A bug: one command that shows it again.",
1486
+ type: "string"
1487
+ },
1488
+ {
1489
+ name: "env",
1490
+ description: "A bug: the place it was seen, one of local, ci, prod, a place `issues.environments` adds, or" + " unknown. Left out, ci when the CI variable is set and local otherwise.",
1491
+ type: "string"
1492
+ },
1493
+ {
1494
+ name: "project",
1495
+ description: "A bug: the project, where one tracker holds several.",
1496
+ type: "string"
1497
+ },
1498
+ {
1499
+ name: "path",
1500
+ description: "A bug: the file the error points at, which names the package its Versions section holds. Left" + " out, the first file of the project the artifact names.",
1501
+ type: "string"
1502
+ },
1503
+ {
1504
+ name: "dry-run",
1505
+ description: "Print the title and the body the line would file, resolve no tracker and file nothing.",
1506
+ type: "boolean"
1507
+ }
1508
+ ],
1509
+ examples: [
1510
+ {
1511
+ cmd: 'rafa issue create --type=bug --module=loop --action=start --summary="resume does not find the worktree branch"' + ' --reproduce="rafa loop start --continue after a stop" --cause=git:new-context --proposed=git:branch-not-found --dry-run',
1512
+ note: "Prints the title `loop - start: resume does not find the worktree branch` and the body of the bug, and files nothing."
1513
+ },
1514
+ {
1515
+ cmd: 'rafa issue create --title="Document issue move" --priority=low --output=json',
1516
+ note: "Writes a start event, then a result event whose data holds the tracker and the ref of the issue."
1517
+ },
1518
+ {
1519
+ cmd: 'rafa issue create --title="Faster plan show" --body-file=spec.md',
1520
+ note: "Files an issue whose body is what spec.md holds; --body-file=- reads it from standard input."
1521
+ }
1522
+ ],
1523
+ outputs: ["text", "json"]
1524
+ });
1525
+ var ISSUE_COMMENT_DECLARATION = Object.freeze({
1526
+ name: "issue comment",
1527
+ subject: ISSUE_SUBJECT.name,
1528
+ action: "comment",
1529
+ summary: "comment on one issue on the tracker",
1530
+ description: "Posts `--body` as a comment on one issue on the tracker `tracker.default` names in" + " `.rafa/config.yaml`, or on the first `tracker.fallback` kind whose preflight passes when it fails," + " each kind passed over warned about. The id is the issue number on that tracker, so on a tracker" + " fallen back to it names a different issue. With `--output=json` the tracker and the ref commented" + " on are the data of the terminal result event.",
1531
+ args: [
1532
+ {
1533
+ name: "id",
1534
+ description: "The issue id on the tracker: a GitHub issue number, or the number of a file under `.rafa/issues/`.",
1535
+ type: "string",
1536
+ required: true
1537
+ }
1538
+ ],
1539
+ flags: [
1540
+ {
1541
+ name: "body",
1542
+ description: "The comment, as markdown. Required.",
1543
+ type: "string",
1544
+ required: true
1545
+ }
1546
+ ],
1547
+ examples: [
1548
+ {
1549
+ cmd: 'rafa issue comment 12 --body="Seen again after the retry change."',
1550
+ note: "Posts one comment on issue 12 on the tracker the chain lands on."
1551
+ },
1552
+ {
1553
+ cmd: "rafa issue comment 12 --body=Reproduced --output=json",
1554
+ note: "Writes a start event, then a result event whose data holds the tracker and the ref commented on."
1555
+ }
1556
+ ],
1557
+ outputs: ["text", "json"]
1558
+ });
1559
+ var ISSUE_MOVE_DECLARATION = Object.freeze({
1560
+ name: "issue move",
1561
+ subject: ISSUE_SUBJECT.name,
1562
+ action: "move",
1563
+ summary: "move one issue on the tracker to a state",
1564
+ description: "Moves one issue on the tracker `tracker.default` names in `.rafa/config.yaml`, or on the first" + " `tracker.fallback` kind whose preflight passes when it fails, each kind passed over warned about," + ` to one of the states ${ISSUE_STATE_WORDS.join(", ")}. The id is the issue number on that tracker, so on` + " a tracker fallen back to it names a different issue. The github tracker holds an issue open or" + " closed, so it warns for backlog, in-progress, in-review and released, which it cannot hold, and" + " still exits 0. With `--output=json` the tracker, the ref, the state and the warning are the data" + " of the terminal result event.",
1565
+ args: [
1566
+ {
1567
+ name: "id",
1568
+ description: "The issue id on the tracker: a GitHub issue number, or the number of a file under `.rafa/issues/`.",
1569
+ type: "string",
1570
+ required: true
1571
+ },
1572
+ {
1573
+ name: "state",
1574
+ description: `The state to move the issue to: one of ${ISSUE_STATE_WORDS.join(", ")}.`,
1575
+ type: "string",
1576
+ required: true
1577
+ }
1578
+ ],
1579
+ flags: [],
1580
+ examples: [
1581
+ {
1582
+ cmd: "rafa issue move 12 done",
1583
+ note: "Moves issue 12 to done, which closes it on the github tracker."
1584
+ },
1585
+ {
1586
+ cmd: "rafa issue move 12 in-progress --output=json",
1587
+ note: "Writes a start event, then a result event whose data holds the tracker, the ref, the state and any warning."
1588
+ }
1589
+ ],
1590
+ outputs: ["text", "json"]
1591
+ });
1592
+ var ISSUE_READY_DECLARATION = Object.freeze({
1593
+ name: "issue ready",
1594
+ subject: ISSUE_SUBJECT.name,
1595
+ action: "ready",
1596
+ summary: `mark an issue ${ISSUE_READY_LABEL} once its author and its body check out`,
1597
+ description: "Reads the issue on the GitHub board, refuses one opened by an account without write access to" + " the repository, refuses a body that does not fill the spec template, refuses an issue carrying two" + " `epic:` labels (with board.relationships set to labels, the default), and then asks whether to mark it" + ` ${ISSUE_READY_LABEL}. On a yes it swaps the labels in one write, adding ${ISSUE_READY_LABEL} and` + ` removing ${ISSUE_NEEDS_WORK_LABEL}. \`--yes\` makes the swap with no question once every check passes,` + " with or without a terminal, and refuses everything the checks refuse without it; without a terminal and" + " without `--yes` it prints both readings and adds no label. A run that marks the issue, or finds" + " it marked already, ends by naming the one step that follows, which `--no-hint` turns off." + " `--dry-run` makes every check and prints both readings, asking nothing and writing nothing whatever" + " the terminal, exits 2 on a check that refuses, and is refused beside `--yes`. With" + " `--output=json` the report is the data of the terminal result event.",
1598
+ args: [
1599
+ {
1600
+ name: "n",
1601
+ description: "The issue number on the GitHub board.",
1602
+ type: "string",
1603
+ required: true
1604
+ }
1605
+ ],
1606
+ flags: [
1607
+ {
1608
+ name: READY_YES_FLAG,
1609
+ description: `Marks the issue ${ISSUE_READY_LABEL} without asking, once its author and its body check out;` + " a check that refuses still refuses. Needed where standard input is no terminal.",
1610
+ type: "boolean"
1611
+ },
1612
+ {
1613
+ name: READY_DRY_RUN_FLAG,
1614
+ description: "Prints both readings and writes nothing, asking no question whatever the terminal;" + " a check that refuses still exits 2. Not with --yes.",
1615
+ type: "boolean"
1616
+ },
1617
+ ISSUE_HINT_FLAG_SPEC
1618
+ ],
1619
+ examples: [
1620
+ {
1621
+ cmd: "rafa issue ready 57",
1622
+ note: `Checks #57 and asks whether to mark it ${ISSUE_READY_LABEL}.`
1623
+ },
1624
+ {
1625
+ cmd: "rafa issue ready 57 --yes",
1626
+ note: `Checks #57 and marks it ${ISSUE_READY_LABEL} with no question, from a script or an agent.`
1627
+ },
1628
+ {
1629
+ cmd: "rafa issue ready 57 --dry-run",
1630
+ note: "Checks #57 and reads its status in one line, from a script, an agent or CI, marking nothing."
1631
+ }
1632
+ ],
1633
+ outputs: ["text", "json"]
1634
+ });
1635
+ var ISSUE_UNBLOCK_DECLARATION = Object.freeze({
1636
+ name: "issue unblock",
1637
+ subject: ISSUE_SUBJECT.name,
1638
+ action: "unblock",
1639
+ summary: `take ${ISSUE_BLOCKED_LABEL} off an issue whose blockers have all closed`,
1640
+ description: `Reads the "Blocked by: #24 #26" line of an issue labelled ${ISSUE_BLOCKED_LABEL} on the GitHub` + " board, asks the board for each blocker's state, and when every blocker is closed asks whether to" + ` remove ${ISSUE_BLOCKED_LABEL} and removes it on a yes. With a blocker still open it names it and` + ` changes nothing. An issue labelled ${ISSUE_BLOCKED_LABEL} whose "Blocked by:" line is missing, names` + " itself or names an id the board has no issue for is reported and never guessed at. Without a" + " terminal and without `--yes` it asks nothing and writes nothing. `--yes` answers the question yes" + " without putting it, terminal or not, and names the blockers it read closed; it never removes" + ` ${ISSUE_BLOCKED_LABEL} from an issue with a blocker open or a line it refuses to guess at.` + " With `--output=json` the outcome of each issue is the" + " data of the terminal result event. With board.relationships set to native it reads nothing and" + " writes nothing: GitHub clears a blocker by itself when the blocking issue closes.",
1641
+ args: [
1642
+ {
1643
+ name: "n",
1644
+ description: "The issue number on the GitHub board. Left out, `--all` reads every blocked issue.",
1645
+ type: "string",
1646
+ required: false
1647
+ }
1648
+ ],
1649
+ flags: [
1650
+ {
1651
+ name: UNBLOCK_ALL_FLAG,
1652
+ description: `Read every open issue labelled ${ISSUE_BLOCKED_LABEL}, asking about each one in turn.`,
1653
+ type: "boolean"
1654
+ },
1655
+ {
1656
+ name: UNBLOCK_YES_FLAG,
1657
+ description: `Answer the question yes without putting it, so ${ISSUE_BLOCKED_LABEL} comes off every issue` + " whose blockers are all closed, with no terminal needed. An open blocker or an unreadable" + ' "Blocked by:" line still changes nothing.',
1658
+ type: "boolean"
1659
+ }
1660
+ ],
1661
+ examples: [
1662
+ {
1663
+ cmd: "rafa issue unblock 57",
1664
+ note: `Asks whether to remove ${ISSUE_BLOCKED_LABEL} from #57 when every issue its "Blocked by:" line names is closed.`
1665
+ },
1666
+ {
1667
+ cmd: "rafa issue unblock 57 --yes",
1668
+ note: `Removes ${ISSUE_BLOCKED_LABEL} from #57 with no question when every blocker it names is closed.`
1669
+ },
1670
+ {
1671
+ cmd: "rafa issue unblock --all",
1672
+ note: "Walks every open blocked issue on the board, asking about each one whose blockers have all closed."
1673
+ }
1674
+ ],
1675
+ outputs: ["text", "json"]
1676
+ });
1677
+ var ISSUE_CHECK_DECLARATION = Object.freeze({
1678
+ name: "issue check",
1679
+ subject: ISSUE_SUBJECT.name,
1680
+ action: "check",
1681
+ summary: "read the references a spec's saved copy names, each with its state",
1682
+ description: "Reads the saved copy of issue <n> under `specs.dir` and prints every reference it names \u2014 an" + " issue, a file, a declared symbol, a command, a flag, a config key \u2014 with its state against the" + " stamp the copy keeps: ok, new, dangling, suspect, resolved or unknown. A reference the copy keeps no" + " stamp for is stamped on this read. Exits 0 whatever the states are, and plans nothing. A missing" + " saved copy is refused, naming the `rafa plan create --issue=<n>` that writes one. With" + " `--output=json` every reference, with its kind, line, state and fingerprint, is the data of the" + " terminal result event.",
1683
+ args: [
1684
+ {
1685
+ name: "n",
1686
+ description: "The issue number on the GitHub board whose saved copy is read.",
1687
+ type: "string",
1688
+ required: true
1689
+ }
1690
+ ],
1691
+ flags: [
1692
+ {
1693
+ name: STAMP_FLAG,
1694
+ description: "Re-stamp every reference with what its target holds now, so the next check reads each one ok.",
1695
+ type: "boolean"
1696
+ }
1697
+ ],
1698
+ examples: [
1699
+ {
1700
+ cmd: "rafa issue check 151",
1701
+ note: "Prints each reference the saved copy of #151 names with its state, and a count per state."
1702
+ },
1703
+ {
1704
+ cmd: "rafa issue check 151 --stamp",
1705
+ note: "Prints the same reading, then re-stamps every reference as reviewed without planning."
1706
+ },
1707
+ {
1708
+ cmd: "rafa issue check 151 --output=json",
1709
+ note: "Writes a start event, then a result event whose data lists every reference with its kind, line, state and fingerprint."
1710
+ }
1711
+ ],
1712
+ outputs: ["text", "json"]
1713
+ });
1714
+ var ISSUE_EDIT_DECLARATION = Object.freeze({
1715
+ name: "issue edit",
1716
+ subject: ISSUE_SUBJECT.name,
1717
+ action: "edit",
1718
+ summary: "append a dated update to an issue body, or replace its body or title, after four gates",
1719
+ description: "Edits one issue on the tracker the chain lands on, after four gates weighed in order, the first" + " refusal ending the run with nothing written and exit code 2: the account editing must have write" + " access or be in board.trustedAuthors; so must the issue's author; the text added must leak nothing," + " and a complete spec must stay complete; and the issue must be open, its labels and saved spec copies" + " allowing the edit. An append (`--append-file` or `--append`, with `--reason`) adds a block" + " `**Updated <YYYY-MM-DD>, <reason>:**` below the body, leaving the original part byte for byte; the" + " same block already closing the body answers `already`. `--replace-file` replaces the body and" + " `--title` the title. `--priority` sets the priority, removes every other and takes the issue out" + " of triage in one write, alone or beside `--title`, weighed by the gates as a title change; a priority" + " the issue already holds alone answers `already`. After the write the issue is read again: a body that no longer holds what was" + " written answers `conflict` with exit code 1, the bodies read before and after saved under" + " `.rafa/scratch/`. An append on an issue already planned answers `stale-copy` and names" + " `rafa plan create --issue=<n> --refresh`, which it never runs. `--dry-run` prints each gate's" + " reading and the added part and writes nothing. With `--output=json` the report and the tracker are" + " the data of the terminal result event.",
1720
+ args: [
1721
+ {
1722
+ name: "n",
1723
+ description: "The issue number on the tracker.",
1724
+ type: "string",
1725
+ required: true
1726
+ }
1727
+ ],
1728
+ flags: [
1729
+ {
1730
+ name: "append-file",
1731
+ description: "A file holding the text to append, read whole; `-` reads standard input." + " Needs --reason. Not with --append, --replace-file, --title or --priority.",
1732
+ type: "string"
1733
+ },
1734
+ {
1735
+ name: "append",
1736
+ description: "The text to append. Needs --reason. Not with --append-file, --replace-file, --title or --priority.",
1737
+ type: "string"
1738
+ },
1739
+ {
1740
+ name: "reason",
1741
+ description: "Why the update was made, written into its heading. Required with an append, refused otherwise.",
1742
+ type: "string"
1743
+ },
1744
+ {
1745
+ name: "replace-file",
1746
+ description: "A file holding the new body, read whole; `-` reads standard input. Not with an append or --priority.",
1747
+ type: "string"
1748
+ },
1749
+ {
1750
+ name: "title",
1751
+ description: "The new title, alone or beside --replace-file or --priority. Not with an append.",
1752
+ type: "string"
1753
+ },
1754
+ {
1755
+ name: PRIORITY_FLAG,
1756
+ description: `The new priority, one of ${ISSUE_PRIORITY_WORDS.join(", ")}: it replaces the one the issue holds and takes` + " the issue out of triage. Alone or beside --title. Not with an append or --replace-file.",
1757
+ type: "string"
1758
+ },
1759
+ {
1760
+ name: "while-in-development",
1761
+ description: "Allows an append on an issue a loop has claimed, answering stale-copy. Only with an append.",
1762
+ type: "boolean"
1763
+ },
1764
+ {
1765
+ name: "dry-run",
1766
+ description: "Prints the added part and each gate's reading and writes nothing; a refusal still exits 2.",
1767
+ type: "boolean"
1768
+ }
1769
+ ],
1770
+ examples: [
1771
+ {
1772
+ cmd: 'rafa issue edit 57 --append-file=update.md --reason="scope narrowed"',
1773
+ note: "Appends what update.md holds to #57 below a dated heading naming the reason."
1774
+ },
1775
+ {
1776
+ cmd: 'rafa issue edit 57 --append="Also covers --dry-run." --reason=clarified --dry-run',
1777
+ note: "Prints each gate's reading and the block it would append, and writes nothing."
1778
+ },
1779
+ {
1780
+ cmd: 'rafa issue edit 57 --title="Faster plan show"',
1781
+ note: "Changes the title of #57 and leaves its body alone."
1782
+ },
1783
+ {
1784
+ cmd: "rafa issue edit 57 --priority=high --dry-run",
1785
+ note: "Prints each gate's reading and the swap, such as `priority: needs-triage \u2192 high`, and writes nothing."
1786
+ },
1787
+ {
1788
+ cmd: "rafa issue edit 57 --replace-file=spec.md --output=json",
1789
+ note: "Replaces the body of #57; the report and the tracker are the data of the result event."
1790
+ }
1791
+ ],
1792
+ outputs: ["text", "json"]
1793
+ });
1794
+
1795
+ // src/commands/issue/issue-tracker.ts
1796
+ var DEFAULT_ISSUE_SEAMS = Object.freeze({});
1797
+ function lineRefusal(problem, usage) {
1798
+ return new CommandExit(1, `\u274C ${problem}
1799
+ Usage: ${usage}`);
1800
+ }
1801
+ function issueProject(context) {
1802
+ if (context.project === null)
1803
+ throw new Error("rafa issue runs inside a project, and was handed none");
1804
+ return context.project;
1805
+ }
1806
+ async function issueSubjectConfig(project, warn) {
1807
+ try {
1808
+ return (await loadConfig({ root: project.root, home: project.home }, {}, warn)).config;
1809
+ } catch (error) {
1810
+ if (!(error instanceof ConfigError))
1811
+ throw error;
1812
+ throw new CommandExit(1, ["\u274C The config cannot be used:", ...error.problems.map((problem) => ` ${problem}`)].join(`
1813
+ `));
1814
+ }
1815
+ }
1816
+
1817
+ // src/commands/issue/ready.ts
1818
+ var READY_USAGE = "rafa issue ready <n> [--yes | --dry-run]";
1819
+ var READY_WRITE_EXIT = 1;
1820
+ var ISSUE_NUMBER = /^[1-9]\d*$/u;
1821
+ function readyQuestion(issue) {
1822
+ return `Mark #${String(issue)} ${SPEC_READY_LABEL}? [y/N] `;
1823
+ }
1824
+ function trustPassLine(issue, repo, reading) {
1825
+ if (!reading.trusted) {
1826
+ throw new TypeError(`issue ready: ${reading.login} is not trusted, and has no pass to name`);
1827
+ }
1828
+ const because = reading.source === "allow-list" ? "listed in board.trustedAuthors" : `who has write access to ${repo}`;
1829
+ return `#${String(issue)} was opened by ${reading.login}, ${because}`;
1830
+ }
1831
+ function completeLine(issue) {
1832
+ return `#${String(issue)} fills every heading the spec template asks for, with no placeholder left`;
1833
+ }
1834
+ function unaskedMessage(issue) {
1835
+ const number = String(issue);
1836
+ return `#${number} is ready to mark; there is no terminal to ask on, so ${SPEC_READY_LABEL} was not added.` + ` Run rafa issue ready ${number} where an answer can be typed`;
1837
+ }
1838
+ function requireOneEpic(found) {
1839
+ const listed = {
1840
+ number: found.number,
1841
+ title: found.title,
1842
+ body: found.body,
1843
+ state: found.state,
1844
+ stateReason: null,
1845
+ labels: found.labels,
1846
+ type: typeOfLabels(found.labels),
1847
+ module: moduleOfLabels(found.labels)
1848
+ };
1849
+ const several = readEpicProblems([listed]).find((problem) => problem.kind === "several-epic-labels");
1850
+ if (several === undefined)
1851
+ return;
1852
+ throw new CommandExit(READINESS_REFUSAL_EXIT, `issue #${String(found.number)} cannot be marked ${SPEC_READY_LABEL}: ${epicProblemMessage(several)}`);
1853
+ }
1854
+ function readyReport(issue, status, reading, lines) {
1855
+ return Object.freeze({
1856
+ issue,
1857
+ status,
1858
+ author: reading.login,
1859
+ trustedBy: reading.source,
1860
+ trust: lines.trust,
1861
+ checked: lines.checked,
1862
+ message: lines.message
1863
+ });
1864
+ }
1865
+ async function runIssueReady(options) {
1866
+ const { gh, issue, trust, ask } = options;
1867
+ const board = options.board ?? createGhIssueBoard({ gh });
1868
+ const readIssue = options.readIssue ?? createGhSpecIssueReader({ gh });
1869
+ const found = await readIssue(issue);
1870
+ const reading = await requireTrustedBoardAuthor({ kind: "issue", number: issue }, trust, found.author);
1871
+ requireCompleteSpec(issueSource(issue), found.body);
1872
+ if ((options.relationships ?? "labels") === "labels")
1873
+ requireOneEpic(found);
1874
+ const lines = { trust: trustPassLine(issue, trust.repo, reading), checked: completeLine(issue) };
1875
+ if (hasSpecReadyLabel(found.labels)) {
1876
+ return readyReport(issue, "already", reading, {
1877
+ ...lines,
1878
+ message: `#${String(issue)} is already marked ${SPEC_READY_LABEL}`
1879
+ });
1880
+ }
1881
+ if (options.dryRun === true) {
1882
+ return readyReport(issue, "checked", reading, {
1883
+ ...lines,
1884
+ message: `#${String(issue)} is ready to mark; --${READY_DRY_RUN_FLAG} wrote nothing.`
1885
+ });
1886
+ }
1887
+ if (options.yes !== true) {
1888
+ if (ask === null) {
1889
+ return readyReport(issue, "unasked", reading, { ...lines, message: unaskedMessage(issue) });
1890
+ }
1891
+ if (!await ask(readyQuestion(issue))) {
1892
+ return readyReport(issue, "declined", reading, {
1893
+ ...lines,
1894
+ message: `#${String(issue)} was left unmarked`
1895
+ });
1896
+ }
1897
+ }
1898
+ try {
1899
+ await board.swapLabels(issue, SPEC_NEEDS_WORK_LABEL, SPEC_READY_LABEL);
1900
+ } catch (error) {
1901
+ throw new CommandExit(READY_WRITE_EXIT, `\u274C ${SPEC_READY_LABEL} could not be put on #${String(issue)}: ${messageOf(error)}`);
1902
+ }
1903
+ return readyReport(issue, "marked", reading, {
1904
+ ...lines,
1905
+ message: `Marked #${String(issue)} ${SPEC_READY_LABEL}, and took ${SPEC_NEEDS_WORK_LABEL} off it`
1906
+ });
1907
+ }
1908
+ function readReadyIssue(context) {
1909
+ const { args } = context;
1910
+ if (args.length > 1) {
1911
+ throw lineRefusal(`Expected one issue number, got ${String(args.length)}: ${args.join(" ")}`, READY_USAGE);
1912
+ }
1913
+ const word = args[0];
1914
+ if (word === undefined) {
1915
+ throw lineRefusal("Name the issue number to mark ready", READY_USAGE);
1916
+ }
1917
+ if (!ISSUE_NUMBER.test(word)) {
1918
+ throw lineRefusal(`"${word}" is no issue number, which is a whole number from 1`, READY_USAGE);
1919
+ }
1920
+ return Number(word);
1921
+ }
1922
+ function readReadySwitch(context, name) {
1923
+ const value = context.flags[name];
1924
+ if (value === undefined)
1925
+ return false;
1926
+ if (typeof value === "boolean")
1927
+ return value;
1928
+ if (value === "true" || value === "false")
1929
+ return value === "true";
1930
+ throw lineRefusal(`--${name} takes no value, and read "${value}" as one`, READY_USAGE);
1931
+ }
1932
+ function readReadyYes(context) {
1933
+ return readReadySwitch(context, READY_YES_FLAG);
1934
+ }
1935
+ function readReadyDryRun(context) {
1936
+ return readReadySwitch(context, READY_DRY_RUN_FLAG);
1937
+ }
1938
+ var DEFAULT_READY_SEAMS = Object.freeze({});
1939
+ async function markIssueReady(context, seams) {
1940
+ const issue = readReadyIssue(context);
1941
+ const yes = readReadyYes(context);
1942
+ const dryRun = readReadyDryRun(context);
1943
+ if (yes && dryRun) {
1944
+ throw lineRefusal(`--${READY_YES_FLAG} and --${READY_DRY_RUN_FLAG} cannot be used together: one asks for the write and the other for none`, READY_USAGE);
1945
+ }
1946
+ const project = issueProject(context);
1947
+ const config = await issueSubjectConfig(project, (message) => {
1948
+ context.output.warn(message);
1949
+ });
1950
+ const openGh = seams.openGh ?? ((dir) => createGhRunner({ cwd: dir }));
1951
+ const openGit = seams.openGit ?? createGitRunner;
1952
+ const isTerminal = seams.isTerminal ?? (() => process.stdin.isTTY === true);
1953
+ const openPrompter = seams.openPrompter ?? (() => createLinePrompter(process.stdin, process.stderr));
1954
+ const gh = openGh(project.root);
1955
+ const trust = ghBoardTrust({
1956
+ gh,
1957
+ trustedAuthors: config.boardTrustedAuthors,
1958
+ repo: boardRepoLabel(openGit(project.root))
1959
+ });
1960
+ const prompter = lazyPrompter(openPrompter);
1961
+ try {
1962
+ return await runIssueReady({
1963
+ gh,
1964
+ issue,
1965
+ trust,
1966
+ board: createRefreshingGhIssueBoard({ gh, config, warn: (line) => {
1967
+ context.output.warn(line);
1968
+ } }),
1969
+ relationships: config.boardRelationships,
1970
+ yes,
1971
+ dryRun,
1972
+ ask: isTerminal() ? prompter.ask : null
1973
+ });
1974
+ } finally {
1975
+ prompter.close();
1976
+ }
1977
+ }
1978
+ function writeReport(context, report) {
1979
+ context.output.info(report.trust);
1980
+ context.output.info(report.checked);
1981
+ context.output.info(report.message);
1982
+ }
1983
+ function createIssueReadyCommand(seams = DEFAULT_READY_SEAMS) {
1984
+ const command = {
1985
+ ...ISSUE_READY_DECLARATION,
1986
+ run: async (context) => {
1987
+ const made = await markIssueReady(context, seams);
1988
+ if (context.outputMode === "json")
1989
+ context.output.result(made);
1990
+ else
1991
+ writeReport(context, made);
1992
+ if ((made.status === "marked" || made.status === "already") && !readReadyDryRun(context)) {
1993
+ await endWithNextStep(context, seams.ending);
1994
+ }
1995
+ }
1996
+ };
1997
+ return Object.freeze(command);
1998
+ }
1999
+ var command = createIssueReadyCommand();
2000
+ var ready_default = command;
2001
+ export {
2002
+ trustPassLine,
2003
+ runIssueReady,
2004
+ requireOneEpic,
2005
+ readyQuestion,
2006
+ readReadyYes,
2007
+ readReadyIssue,
2008
+ readReadyDryRun,
2009
+ markIssueReady,
2010
+ ready_default as default,
2011
+ createIssueReadyCommand,
2012
+ completeLine,
2013
+ READY_WRITE_EXIT,
2014
+ READY_USAGE,
2015
+ DEFAULT_READY_SEAMS
2016
+ };