agent-kitchen 0.5.0 → 0.6.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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file What the repo wants of each piece, kept in `.agents/kitchen.json`: `true` to have it,
3
+ * `false` to leave it out for good. Only the pieces that are a choice carry a decision;
4
+ * `skills-manager` does not, because it is the tool that installs the others' skills and is
5
+ * always kept up to date, never opted in or out.
6
+ */
7
+ import { isObject, readJson, writeJson } from "./repo.js";
8
+ import { NAME as AGENTS_MD } from "./pieces/agents-md.js";
9
+ import { NAME as CLOUD } from "./pieces/cloud.js";
10
+ import { NAME as GUARD } from "./pieces/guard.js";
11
+ import { NAME as ISSUE_FLOW } from "./pieces/issue-flow.js";
12
+ import { NAME as SKILLS } from "./pieces/skills.js";
13
+ /** The file with the decisions, relative to the repo. */
14
+ export const DECISIONS = ".agents/kitchen.json";
15
+ /** The pieces a repo can decide on, in the order sync runs them. */
16
+ export const DECIDABLE = [ISSUE_FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
17
+ /**
18
+ * Reads the decisions of a repo.
19
+ * @param root - The repo.
20
+ * @returns The decisions, or null if the file does not exist.
21
+ * @throws KitError if the file is not valid JSON.
22
+ * @example readDecisions(root)?.pieces // { "claude-repo-guard": false }
23
+ */
24
+ export function readDecisions(root) {
25
+ const raw = readJson(root, DECISIONS);
26
+ if (raw === null)
27
+ return null;
28
+ const stored = isObject(raw.pieces) ? raw.pieces : {};
29
+ const pieces = {};
30
+ const warnings = [];
31
+ for (const name of DECIDABLE) {
32
+ if (!(name in stored))
33
+ continue;
34
+ const value = stored[name];
35
+ if (typeof value === "boolean")
36
+ pieces[name] = value;
37
+ else
38
+ warnings.push(`${DECISIONS}: "${name}" no es true ni false; se trata como sin decisión.`);
39
+ }
40
+ return { pieces, raw, warnings };
41
+ }
42
+ /**
43
+ * The decidable pieces with no decision yet.
44
+ * @param saved - What was read, or null if there is no file.
45
+ * @returns Their names; all of them without a file.
46
+ * @example undecided(null) // ["issue-flow", "claude-cloud", ...]
47
+ */
48
+ export function undecided(saved) {
49
+ return DECIDABLE.filter((name) => saved?.pieces[name] === undefined);
50
+ }
51
+ /**
52
+ * The pieces the repo decided not to have.
53
+ * @param saved - What was read, or null if there is no file.
54
+ * @returns Their names.
55
+ * @example off({ pieces: { skills: false }, raw: {}, warnings: [] }) // ["skills"]
56
+ */
57
+ export function off(saved) {
58
+ return DECIDABLE.filter((name) => saved?.pieces[name] === false);
59
+ }
60
+ /**
61
+ * Settles the pieces that have no decision from what the repo has and what was chosen.
62
+ * @param statuses - What each piece found.
63
+ * @param chosen - The steps the user picked.
64
+ * @param previous - The decisions already made; they never change here.
65
+ * @returns `previous` plus a decision for each decidable piece without one: installed or
66
+ * with a chosen step is true, offered steps all unchosen is false, no steps leaves it open.
67
+ * @example decide([{ name: "skills", installed: true, summary: "", state: "", steps: [] }], [], {}) // { skills: true }
68
+ */
69
+ export function decide(statuses, chosen, previous) {
70
+ const result = { ...previous };
71
+ for (const status of statuses) {
72
+ if (!DECIDABLE.includes(status.name) || status.name in previous)
73
+ continue;
74
+ if (status.installed || chosen.some((step) => step.piece === status.name))
75
+ result[status.name] = true;
76
+ else if (status.steps.length > 0)
77
+ result[status.name] = false;
78
+ }
79
+ return result;
80
+ }
81
+ /**
82
+ * Writes the decisions, keeping every other key of the file.
83
+ * @param root - The repo.
84
+ * @param pieces - The decisions to write.
85
+ * @param raw - The whole file as it was read (empty if there was none).
86
+ * @example writeDecisions(root, { "issue-flow": true }, {})
87
+ */
88
+ export function writeDecisions(root, pieces, raw) {
89
+ const rawPieces = isObject(raw.pieces) ? raw.pieces : {};
90
+ writeJson(root, DECISIONS, { ...raw, pieces: { ...rawPieces, ...pieces } });
91
+ }
@@ -0,0 +1,53 @@
1
+ /** What to do with the changes: leave them, commit on the base, on a branch, or a branch with a PR. */
2
+ export type FinishChoice = "leave" | "base" | "branch" | "pr";
3
+ /**
4
+ * The base branch, as issue-flow takes it: `baseBranch` of `.agents/flow.json`, or origin's
5
+ * default as git knows it, or `main`.
6
+ * @param root - The repo.
7
+ * @returns The branch.
8
+ * @example baseBranch(root) // "main"
9
+ */
10
+ export declare function baseBranch(root: string): string;
11
+ /**
12
+ * The files that differ from HEAD, tracked or not, one by one, without the repo's worktrees.
13
+ * @param root - The repo.
14
+ * @returns Their paths.
15
+ * @example changedPaths(root) // ["package.json", "package-lock.json"]
16
+ */
17
+ export declare function changedPaths(root: string): string[];
18
+ /**
19
+ * Checks, before any step, that the repo has no changes and is on its base branch.
20
+ * @param root - The repo.
21
+ * @param base - The base branch.
22
+ * @throws KitError saying what is wrong and what to do.
23
+ */
24
+ export declare function checkReady(root: string, base: string): void;
25
+ /**
26
+ * The branch for today's changes: a new name, also against origin's branches git knows of.
27
+ * @param root - The repo.
28
+ * @param date - Today.
29
+ * @returns `chore/agent-kitchen-apply-AAAA-MM-DD`, or with `-2`, `-3`… if taken.
30
+ * @example branchName(root, new Date()) // "chore/agent-kitchen-apply-2026-11-01"
31
+ */
32
+ export declare function branchName(root: string, date: Date): string;
33
+ /** What `finish` needs to know. */
34
+ export interface FinishOptions {
35
+ base: string;
36
+ branch: string;
37
+ steps: string[];
38
+ paths: string[];
39
+ manager: string;
40
+ packageChanged?: boolean;
41
+ }
42
+ /**
43
+ * Does what the person chose with the changes.
44
+ * @param root - The repo, on the base, with the changes of the run.
45
+ * @param choice - What to do.
46
+ * @param options - The base, the new branch's name, the steps that ran, the files to commit (the ones shown), the package manager and whether package.json changed.
47
+ * @returns The lines to show and whether it all worked.
48
+ * @example await finish(root, "pr", { base: "main", branch: "chore/agent-kitchen-apply-2026-11-01", steps, manager: "npm" })
49
+ */
50
+ export declare function finish(root: string, choice: FinishChoice, options: FinishOptions): Promise<{
51
+ ok: boolean;
52
+ lines: string[];
53
+ }>;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @file What `apply --commit` does with git: before any step, it checks that the repo has no
3
+ * changes (the repo's worktrees under it do not count) and is on its base branch; after the
4
+ * steps, it commits what changed where the person chose. A new branch is made by passing
5
+ * through it for the length of the command: `git switch -c`, the commit there (so a hook that
6
+ * forbids commits on the base does not stop it), the push and the PR if asked, and back to
7
+ * the base, which never moves. git runs its hooks; nothing here skips them.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join, relative } from "node:path";
13
+ import { KitError } from "./output.js";
14
+ import { FLOW_CONFIG } from "./piece.js";
15
+ import { dependencyVersion, readJson } from "./repo.js";
16
+ import { captureNpxWithout, isRelease, npmLatest } from "./tools.js";
17
+ /** The subject of the commit and the title of the PR. */
18
+ const TITLE = "chore: agent-kitchen apply";
19
+ /** github-rest on npm, which opens the PR (REST, never gh's GraphQL). */
20
+ const GITHUB_REST = "@ramon-sg/github-rest";
21
+ /**
22
+ * Runs git in the repo.
23
+ * @param root - The repo.
24
+ * @param args - The arguments.
25
+ * @param inherit - True to show its output on the terminal, for a push that may ask for credentials.
26
+ * @returns How it went.
27
+ */
28
+ function git(root, args, inherit = false) {
29
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", stdio: inherit ? "inherit" : "pipe" });
30
+ return { ok: result.status === 0, out: (result.stdout ?? "").trimEnd(), err: (result.stderr ?? "").trim() };
31
+ }
32
+ /**
33
+ * The base branch, as issue-flow takes it: `baseBranch` of `.agents/flow.json`, or origin's
34
+ * default as git knows it, or `main`.
35
+ * @param root - The repo.
36
+ * @returns The branch.
37
+ * @example baseBranch(root) // "main"
38
+ */
39
+ export function baseBranch(root) {
40
+ const flow = readJson(root, FLOW_CONFIG);
41
+ if (typeof flow?.baseBranch === "string" && flow.baseBranch !== "")
42
+ return flow.baseBranch;
43
+ const head = git(root, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
44
+ return head.ok && head.out.startsWith("origin/") ? head.out.slice("origin/".length) : "main";
45
+ }
46
+ /**
47
+ * The repo's other worktrees that live inside it, relative to it.
48
+ * @param root - The repo.
49
+ * @returns Their folders, like `.claude/worktrees/feat+1-x`.
50
+ */
51
+ function innerWorktrees(root) {
52
+ const list = git(root, ["worktree", "list", "--porcelain"]);
53
+ if (!list.ok)
54
+ return [];
55
+ return list.out
56
+ .split("\n")
57
+ .filter((line) => line.startsWith("worktree "))
58
+ .map((line) => relative(root, line.slice("worktree ".length)))
59
+ .filter((path) => path !== "" && !path.startsWith(".."));
60
+ }
61
+ /**
62
+ * The files that differ from HEAD, tracked or not, one by one, without the repo's worktrees.
63
+ * @param root - The repo.
64
+ * @returns Their paths.
65
+ * @example changedPaths(root) // ["package.json", "package-lock.json"]
66
+ */
67
+ export function changedPaths(root) {
68
+ const status = git(root, ["status", "--porcelain", "-uall", "-z"]);
69
+ if (!status.ok)
70
+ throw new KitError(`no pude leer git status: ${status.err}`);
71
+ const worktrees = innerWorktrees(root);
72
+ const entries = status.out.split("\0").filter((entry) => entry !== "");
73
+ const paths = [];
74
+ for (let index = 0; index < entries.length; index += 1) {
75
+ const entry = entries[index] ?? "";
76
+ if (/[RC]/.test(entry.slice(0, 2)))
77
+ index += 1;
78
+ const path = entry.slice(3).replace(/\/$/, "");
79
+ if (!worktrees.some((tree) => path === tree || path.startsWith(`${tree}/`)))
80
+ paths.push(path);
81
+ }
82
+ return paths;
83
+ }
84
+ /**
85
+ * Checks, before any step, that the repo has no changes and is on its base branch.
86
+ * @param root - The repo.
87
+ * @param base - The base branch.
88
+ * @throws KitError saying what is wrong and what to do.
89
+ */
90
+ export function checkReady(root, base) {
91
+ const branch = git(root, ["branch", "--show-current"]).out;
92
+ if (branch === "")
93
+ throw new KitError(`--commit necesita estar en ${base}, y HEAD no está en ninguna rama.`);
94
+ if (branch !== base)
95
+ throw new KitError(`--commit necesita estar en ${base}, la rama base, y estás en ${branch}.`);
96
+ const paths = changedPaths(root);
97
+ if (paths.length > 0)
98
+ throw new KitError(`--commit necesita el repo sin cambios, y hay cambios en: ${paths.join(", ")}. Commitéalos o guárdalos aparte y corre otra vez.`);
99
+ }
100
+ /**
101
+ * The branch for today's changes: a new name, also against origin's branches git knows of.
102
+ * @param root - The repo.
103
+ * @param date - Today.
104
+ * @returns `chore/agent-kitchen-apply-AAAA-MM-DD`, or with `-2`, `-3`… if taken.
105
+ * @example branchName(root, new Date()) // "chore/agent-kitchen-apply-2026-11-01"
106
+ */
107
+ export function branchName(root, date) {
108
+ const pad = (n) => String(n).padStart(2, "0");
109
+ const name = `chore/agent-kitchen-apply-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
110
+ const taken = (candidate) => git(root, ["rev-parse", "-q", "--verify", `refs/heads/${candidate}`]).ok || git(root, ["rev-parse", "-q", "--verify", `refs/remotes/origin/${candidate}`]).ok;
111
+ if (!taken(name))
112
+ return name;
113
+ let n = 2;
114
+ while (taken(`${name}-${n}`))
115
+ n += 1;
116
+ return `${name}-${n}`;
117
+ }
118
+ /**
119
+ * Stages the files shown to the person, taken literally, and commits them, with the hooks.
120
+ * @param root - The repo.
121
+ * @param paths - The files.
122
+ * @param steps - The steps that ran, for the body.
123
+ * @returns How git went.
124
+ */
125
+ function commit(root, paths, steps) {
126
+ const add = git(root, ["--literal-pathspecs", "add", "-A", "--", ...paths]);
127
+ if (!add.ok)
128
+ return add;
129
+ return git(root, ["commit", "-q", "-m", TITLE, "-m", steps.map((step) => `- ${step}`).join("\n")]);
130
+ }
131
+ /**
132
+ * The version of github-rest to open the PR with: the repo's exact one, or npm's latest.
133
+ * @param root - The repo.
134
+ * @returns The version, or null if neither is known.
135
+ */
136
+ async function githubRestVersion(root) {
137
+ const own = dependencyVersion(readJson(root, "package.json"), GITHUB_REST);
138
+ if (own !== null && isRelease(own))
139
+ return own;
140
+ return npmLatest(GITHUB_REST);
141
+ }
142
+ /**
143
+ * Opens the PR with github-rest, without GH_REPO, so the repo is origin's.
144
+ * @param root - The repo, on the branch.
145
+ * @param branch - The branch.
146
+ * @param base - The base.
147
+ * @param steps - The steps, for the body.
148
+ * @returns The PR's URL, or why it failed.
149
+ */
150
+ async function openPr(root, branch, base, steps) {
151
+ const version = await githubRestVersion(root);
152
+ if (version === null)
153
+ return { error: `no sé qué versión de ${GITHUB_REST} usar.` };
154
+ const dir = mkdtempSync(join(tmpdir(), "agent-kitchen-pr-"));
155
+ const bodyFile = join(dir, "body.md");
156
+ writeFileSync(bodyFile, `Cambios de \`agent-kitchen apply\`:\n\n${steps.map((step) => `- ${step}`).join("\n")}\n`);
157
+ try {
158
+ const result = await captureNpxWithout(root, [`${GITHUB_REST}@${version}`, "pr", "create", "--head", branch, "--base", base, "--title", TITLE, "--body-file", bodyFile], ["GH_REPO"]);
159
+ const url = result.out.trim().split("\n").at(-1) ?? "";
160
+ return result.status === 0 && url !== "" ? { url } : { error: `${result.err}${result.out}`.trim() || "github-rest falló sin decir por qué." };
161
+ }
162
+ finally {
163
+ rmSync(dir, { recursive: true, force: true });
164
+ }
165
+ }
166
+ /**
167
+ * What the person can run to open the PR by hand.
168
+ * @param branch - The branch.
169
+ * @param base - The base.
170
+ * @returns The command.
171
+ */
172
+ function prCommand(branch, base) {
173
+ return `npx @ramon-sg/github-rest pr create --head ${branch} --base ${base} --title "${TITLE}" --body "..."`;
174
+ }
175
+ /**
176
+ * Pushes the branch and opens its PR, saying what to run if either fails.
177
+ * @param root - The repo, on the branch.
178
+ * @param branch - The branch.
179
+ * @param base - The base.
180
+ * @param steps - The steps, for the body.
181
+ * @param lines - Where to add what happened.
182
+ * @returns Whether both worked.
183
+ */
184
+ async function pushAndOpen(root, branch, base, steps, lines) {
185
+ const pushed = git(root, ["push", "-u", "origin", branch], true);
186
+ if (!pushed.ok) {
187
+ lines.push(`No pude subir la rama. Falta: git push -u origin ${branch}, y luego ${prCommand(branch, base)}`);
188
+ return false;
189
+ }
190
+ const pr = await openPr(root, branch, base, steps);
191
+ if ("url" in pr) {
192
+ lines.push(`PR: ${pr.url}`);
193
+ return true;
194
+ }
195
+ lines.push(`La rama está en origin, pero no pude abrir el PR: ${pr.error}`, `Falta: ${prCommand(branch, base)}`);
196
+ return false;
197
+ }
198
+ /**
199
+ * Does what the person chose with the changes.
200
+ * @param root - The repo, on the base, with the changes of the run.
201
+ * @param choice - What to do.
202
+ * @param options - The base, the new branch's name, the steps that ran, the files to commit (the ones shown), the package manager and whether package.json changed.
203
+ * @returns The lines to show and whether it all worked.
204
+ * @example await finish(root, "pr", { base: "main", branch: "chore/agent-kitchen-apply-2026-11-01", steps, manager: "npm" })
205
+ */
206
+ export async function finish(root, choice, options) {
207
+ const { base, branch, steps, paths } = options;
208
+ if (choice === "leave")
209
+ return { ok: true, lines: ["Falta: revisa el diff y haz commit."] };
210
+ if (choice === "base") {
211
+ const done = commit(root, paths, steps);
212
+ if (!done.ok)
213
+ return { ok: false, lines: [`No pude commitear en ${base}; los cambios quedan sin commit.`, done.err || done.out] };
214
+ return { ok: true, lines: [`Commit en ${base}. Falta: git push cuando quieras.`] };
215
+ }
216
+ const moved = git(root, ["switch", "-q", "-c", branch]);
217
+ if (!moved.ok)
218
+ return { ok: false, lines: [`No pude crear la rama ${branch}; los cambios quedan sin commit.`, moved.err] };
219
+ const lines = [];
220
+ let ok = true;
221
+ let committed = false;
222
+ try {
223
+ const done = commit(root, paths, steps);
224
+ if (!done.ok) {
225
+ ok = false;
226
+ lines.push("No pude commitear.", done.err || done.out);
227
+ }
228
+ else {
229
+ committed = true;
230
+ lines.push(`Commit en la rama ${branch}.`);
231
+ if (choice === "pr")
232
+ ok = await pushAndOpen(root, branch, base, steps, lines);
233
+ }
234
+ }
235
+ finally {
236
+ const back = git(root, ["switch", "-q", base]);
237
+ if (!back.ok) {
238
+ ok = false;
239
+ lines.push(`No pude volver a ${base}: ${back.err}. Sigues en ${branch}.`);
240
+ }
241
+ else if (!committed) {
242
+ git(root, ["branch", "-q", "-d", branch]);
243
+ lines.push(`Vuelves a ${base} con los cambios sin commit.`);
244
+ }
245
+ else
246
+ lines.push(`Vuelves a ${base}, sin los cambios: están en ${branch}.`);
247
+ }
248
+ if (!committed)
249
+ return { ok: false, lines };
250
+ if (options.packageChanged === true)
251
+ lines.push(`node_modules quedó con lo de la rama; si sigues en ${base}, corre '${options.manager} install'.`);
252
+ return { ok, lines };
253
+ }
package/dist/menu.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { FinishChoice } from "./git-finish.js";
1
2
  import type { Candidate, Step } from "./piece.js";
2
3
  /** What the person wants after seeing the table. */
3
4
  export type Path = "all" | "pick" | "leave";
@@ -14,6 +15,20 @@ export declare function menuOptions(steps: Step[]): {
14
15
  }[];
15
16
  initialValues: number[];
16
17
  };
18
+ /**
19
+ * The pieces menu's options: each piece by its name, ticked if the repo uses it.
20
+ * @param names - The pieces, in the order to show them.
21
+ * @param ticked - The ones ticked at the start.
22
+ * @returns The options and the ones ticked at the start.
23
+ * @example pieceOptions(["issue-flow", "skills"], ["skills"]).initialValues // ["skills"]
24
+ */
25
+ export declare function pieceOptions(names: string[], ticked: string[]): {
26
+ options: {
27
+ value: string;
28
+ label: string;
29
+ }[];
30
+ initialValues: string[];
31
+ };
17
32
  /**
18
33
  * The skills menu's options: grouped by catalog, each with its description cut short.
19
34
  * @param candidates - What the catalogs offer.
@@ -39,6 +54,14 @@ export declare function askPath(count: number): Promise<Path>;
39
54
  * @example await askSteps(steps)
40
55
  */
41
56
  export declare function askSteps(steps: Step[]): Promise<Step[] | null>;
57
+ /**
58
+ * Asks which pieces the repo uses.
59
+ * @param names - The pieces.
60
+ * @param ticked - The ones ticked at the start.
61
+ * @returns The picked pieces, or null if the person cancelled.
62
+ * @example await askPieces(["issue-flow", "skills"], ["skills"])
63
+ */
64
+ export declare function askPieces(names: string[], ticked: string[]): Promise<string[] | null>;
42
65
  /**
43
66
  * Asks which skills to install.
44
67
  * @param candidates - What the catalogs offer.
@@ -54,3 +77,13 @@ export declare function askSkills(candidates: Candidate[], ticked: string[]): Pr
54
77
  * @example await askReplace(["planning"])
55
78
  */
56
79
  export declare function askReplace(own: string[]): Promise<boolean | null>;
80
+ /**
81
+ * Asks, after the steps, what to do with the changes. Leaving them is the default, and
82
+ * cancelling leaves them too.
83
+ * @param base - The base branch.
84
+ * @param branch - The name of the new branch.
85
+ * @param failed - Whether a step failed, to warn above the question.
86
+ * @returns The answer.
87
+ * @example await askFinish("main", "chore/agent-kitchen-apply-2026-11-01", false) // "pr"
88
+ */
89
+ export declare function askFinish(base: string, branch: string, failed: boolean): Promise<FinishChoice>;
package/dist/menu.js CHANGED
@@ -28,6 +28,16 @@ export function menuOptions(steps) {
28
28
  initialValues: steps.flatMap((step, index) => (step.checked ? [index] : [])),
29
29
  };
30
30
  }
31
+ /**
32
+ * The pieces menu's options: each piece by its name, ticked if the repo uses it.
33
+ * @param names - The pieces, in the order to show them.
34
+ * @param ticked - The ones ticked at the start.
35
+ * @returns The options and the ones ticked at the start.
36
+ * @example pieceOptions(["issue-flow", "skills"], ["skills"]).initialValues // ["skills"]
37
+ */
38
+ export function pieceOptions(names, ticked) {
39
+ return { options: names.map((name) => ({ value: name, label: name })), initialValues: names.filter((name) => ticked.includes(name)) };
40
+ }
31
41
  /**
32
42
  * The skills menu's options: grouped by catalog, each with its description cut short.
33
43
  * @param candidates - What the catalogs offer.
@@ -72,6 +82,19 @@ export async function askSteps(steps) {
72
82
  return null;
73
83
  return steps.filter((_, index) => picked.includes(index));
74
84
  }
85
+ /**
86
+ * Asks which pieces the repo uses.
87
+ * @param names - The pieces.
88
+ * @param ticked - The ones ticked at the start.
89
+ * @returns The picked pieces, or null if the person cancelled.
90
+ * @example await askPieces(["issue-flow", "skills"], ["skills"])
91
+ */
92
+ export async function askPieces(names, ticked) {
93
+ spanishKeys();
94
+ const { options, initialValues } = pieceOptions(names, ticked);
95
+ const picked = await multiselect({ message: "¿Qué piezas usa este repo?", options, initialValues, required: false });
96
+ return isCancel(picked) ? null : picked;
97
+ }
75
98
  /**
76
99
  * Asks which skills to install.
77
100
  * @param candidates - What the catalogs offer.
@@ -98,3 +121,26 @@ export async function askReplace(own) {
98
121
  });
99
122
  return isCancel(answer) ? null : answer;
100
123
  }
124
+ /**
125
+ * Asks, after the steps, what to do with the changes. Leaving them is the default, and
126
+ * cancelling leaves them too.
127
+ * @param base - The base branch.
128
+ * @param branch - The name of the new branch.
129
+ * @param failed - Whether a step failed, to warn above the question.
130
+ * @returns The answer.
131
+ * @example await askFinish("main", "chore/agent-kitchen-apply-2026-11-01", false) // "pr"
132
+ */
133
+ export async function askFinish(base, branch, failed) {
134
+ spanishKeys();
135
+ const answer = await select({
136
+ message: failed ? "Algún paso falló. ¿Qué hago con los cambios?" : "¿Qué hago con los cambios?",
137
+ initialValue: "leave",
138
+ options: [
139
+ { value: "leave", label: "Dejarlos sin commit" },
140
+ { value: "base", label: `Commit en ${base}` },
141
+ { value: "branch", label: `Commit en una rama nueva, ${branch}`, hint: `${base} queda como está` },
142
+ { value: "pr", label: "Commit en una rama nueva, push y PR", hint: `contra ${base}` },
143
+ ],
144
+ });
145
+ return isCancel(answer) ? "leave" : answer;
146
+ }
@@ -1,11 +1,17 @@
1
1
  /**
2
2
  * @file The issue flow (`@ramon-sg/issue-flow`): its CLI, hooks and config, and its skills.
3
- * The repo's version is the one in package.json. Installing runs its `init` at the latest
4
- * version; updating runs its `upgrade` at the new one and then moves its skills to the same
5
- * tag, because the skills call the CLI. If only the skills are off, it moves them alone.
6
- * From 0.2.6 it also offers `labels sync`, after the install or update, when GitHub lacks
7
- * some of the labels the flow expects.
8
- * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
3
+ * The repo's version is the one in package.json. From 0.3.0, installing and updating run its
4
+ * `apply` at the latest version, and reading its `--json` tells what it kept as it was; at
5
+ * the repo's version, `apply` also repairs hooks or a `$schema` its doctor finds behind (a
6
+ * version raised by hand or by Dependabot). Before 0.3.0, installing runs `init` and
7
+ * updating runs `upgrade`. After updating or repairing, and after installing with `apply`,
8
+ * the flow's skills move to the same tag, because they call the CLI. If only the skills are
9
+ * off, they move alone. From 0.2.6 it also offers `labels sync`, after the install or
10
+ * update, when GitHub lacks some of the labels the flow expects. From 0.3.0, whose `apply`
11
+ * keeps the repo's github-rest, it also raises an exact github-rest below the floor of the
12
+ * range that version of the flow declares on npm, as the flow itself reads it.
13
+ * `apply` and `init` run with `--yes`: the person answered agent-kitchen's questions before
14
+ * the steps.
9
15
  */
10
16
  import { type Check, type PieceStatus } from "../piece.js";
11
17
  /** The piece's name, as `--skip` takes it. */