@ramon-sg/issue-flow 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@ Flujo de trabajo de issue a PR para cualquier repo, en local y en las sesiones d
8
8
 
9
9
  - **`issue-flow init|doctor|upgrade`:** escribe, revisa y mueve a su versión lo que el flujo deja en el repo: `.agents/flow.json` con sus recetas, las dependencias y los hooks. Los corre `agent-kitchen`.
10
10
  - **`issue-flow issue branch <n>`:** propone la rama de una issue (con Claude por defecto, o desde el título). Con `--missing`, agrega el slug de rama a todas las issues abiertas que no lo tienen.
11
+ - **`issue-flow labels sync`:** crea en GitHub los labels que el flujo espera y le faltan al repo: los de tipo y `P1` a `P4`, con los mismos colores en todos los repos.
11
12
  - **`issue-flow issue start <n>`:** crea la rama y el worktree, hace el commit inicial, abre el PR en draft, asigna la issue y abre Claude. Con `--cloud`, lo hace en una sesión en la nube.
12
13
  - **`issue-flow worktree setup|teardown|ports|cleanup|prune`:** prepara y limpia cada worktree con los pasos del repo, le da un bloque de puertos y borra los worktrees con el PR cerrado.
13
14
  - **`issue-flow pr worktree <n>`:** abre un PR en su propio worktree, en la rama del PR, para revisarlo sin soltar lo que estás haciendo.
@@ -270,6 +271,40 @@ Todo es opcional; sin el archivo valen los valores por defecto. Una clave descon
270
271
  | `start.cloudPrompt` | `/start-issue {number} --yes{flags}` | Lo que `--cloud` le pide a la sesión; `{flags}` es `--new` o `--resume <rama>` |
271
272
  | `plans.dirs` | `docs/plans`, `docs/specs` | Dónde buscar el plan de la issue: lo que tocó la rama, o el documento cuyo `prs:` nombra su PR |
272
273
  | `colors` | `critical-path` y `P1:` rojo, `P2:` naranjo, `P3:` amarillo, `P4:` cian | Color de la sesión por prefijo de label; gana el primero que calce |
274
+ | `labels` | Los 8 de abajo | Labels que crea `labels sync`; lo del repo se suma al default (ver abajo) |
275
+
276
+ ## Labels: `issue-flow labels sync`
277
+
278
+ ```sh
279
+ npx @ramon-sg/issue-flow labels sync --dry-run # qué haría
280
+ npx @ramon-sg/issue-flow labels sync # crea los que faltan
281
+ npx @ramon-sg/issue-flow labels sync --update # también corrige color y descripción
282
+ ```
283
+
284
+ Crea en GitHub los labels de `labels` que el repo no tiene. Los nombres se comparan sin mayúsculas, como en GitHub: un `Bug` cuenta como `bug` y nunca se renombra. Uno que existe con otro color o descripción solo se avisa, salvo con `--update`. Nunca borra. `--json` da la lista con el estado de cada uno: `created`, `updated`, `ok`, `differs`, o `to-create` y `to-update` con `--dry-run`. Si GitHub rechaza uno, sigue con los demás, lo marca `failed` y sale con 1.
285
+
286
+ | Label | Color | Descripción |
287
+ |---|---|---|
288
+ | `bug` | `d73a4a` | Something isn't working |
289
+ | `enhancement` | `a2eeef` | New feature or request |
290
+ | `documentation` | `0075ca` | Improvements or additions to documentation |
291
+ | `spike` | `fbca04` | Time-boxed research before building |
292
+ | `P1: urgent` | `B60205` | Breaks production or blocks work |
293
+ | `P2: important` | `F9D0C4` | Real impact, with some slack |
294
+ | `P3: minor` | `FEF2C0` | Default when in doubt |
295
+ | `P4: low` | `BFDADC` | Nice-to-have |
296
+
297
+ Los tres primeros son los de GitHub, con sus colores y textos. A diferencia de `branch.types` y `colors`, lo que pone el repo **se suma** al default: un nombre reemplaza al de por defecto con ese nombre, y `null` lo quita. La taxonomía del repo va acá y no en el `project.md` de `github-issues`, porque con `claude-repo-guard` el agente no puede escribir en `.agents/skills/`:
298
+
299
+ ```json
300
+ {
301
+ "labels": {
302
+ "area:api": { "color": "1d76db", "description": "packages/api" },
303
+ "size:S": { "color": "c5def5", "description": "Menos de 1 día" },
304
+ "spike": null
305
+ }
306
+ }
307
+ ```
273
308
 
274
309
  ## Desarrollo
275
310
 
package/dist/cli.d.ts CHANGED
@@ -84,6 +84,9 @@ declare const OPTIONS: {
84
84
  readonly recipe: {
85
85
  readonly type: "string";
86
86
  };
87
+ readonly update: {
88
+ readonly type: "boolean";
89
+ };
87
90
  };
88
91
  /** The parsed flags. */
89
92
  export type Flags = ReturnType<typeof parseArgs<{
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { parseArgs } from "node:util";
8
8
  import { GitHubError, InvalidRepoEnvError, resolveRepo } from "@ramon-sg/github-rest";
9
9
  import { branchMissing, branchOf } from "./commands/issue-branch.js";
10
10
  import { issueStart } from "./commands/issue-start.js";
11
+ import { labelsSync } from "./commands/labels.js";
11
12
  import { ADAPTER_COMMANDS, HOOK_COMMANDS, runAdapter, runHook, runSession, runWorktree, WORKTREE_COMMANDS } from "./cli-worktree.js";
12
13
  import { runSetupCommand, SETUP_COMMANDS } from "./cli-setup.js";
13
14
  import { prWorktree } from "./commands/pr-worktree.js";
@@ -41,6 +42,7 @@ const OPTIONS = {
41
42
  next: { type: "boolean" },
42
43
  "no-setup": { type: "boolean" },
43
44
  recipe: { type: "string" },
45
+ update: { type: "boolean" },
44
46
  };
45
47
  /** Flags each command takes, besides `--repo`, `--help` and `--namer`. */
46
48
  const COMMAND_FLAGS = {
@@ -75,6 +77,7 @@ const COMMAND_FLAGS = {
75
77
  init: ["yes", "recipe"],
76
78
  doctor: ["json"],
77
79
  upgrade: ["dry-run"],
80
+ "labels sync": ["update", "dry-run", "json"],
78
81
  };
79
82
  /** An issue number: digits only. */
80
83
  const NUMBER = /^\d+$/;
@@ -216,6 +219,8 @@ export async function main(argv, out, cwd) {
216
219
  throw new UsageError("-- only goes with pr worktree.");
217
220
  if (group === "pr")
218
221
  return runPr(sub, rest, flags, { out, cwd, command });
222
+ if (group === "labels")
223
+ return runLabels(sub, rest, flags, { out, cwd });
219
224
  if (group !== undefined && SETUP_COMMANDS.includes(group)) {
220
225
  ownFlags(group, flags);
221
226
  return runSetupCommand(group, flags, [sub, ...rest].filter((item) => item !== undefined), { out, cwd });
@@ -253,6 +258,24 @@ async function runPr(sub, rest, flags, run) {
253
258
  const config = loadConfig(run.cwd);
254
259
  return prWorktree({ repo: repoOf(flags.repo, config, run.cwd), config, cwd: run.cwd, number: Number(text), setup: !(flags["no-setup"] ?? false), command: run.command }, run.out);
255
260
  }
261
+ /**
262
+ * Runs `issue-flow labels sync`.
263
+ * @param sub - The word after `labels`.
264
+ * @param rest - The positionals after it.
265
+ * @param flags - The flags.
266
+ * @param run - Where it writes and runs.
267
+ * @returns The exit code.
268
+ * @throws UsageError if the arguments do not fit; GitHubError if GitHub fails.
269
+ */
270
+ async function runLabels(sub, rest, flags, run) {
271
+ if (sub !== "sync")
272
+ throw new UsageError("labels expects sync.");
273
+ ownFlags("labels sync", flags);
274
+ if (rest.length > 0)
275
+ throw new UsageError("labels sync takes no arguments.");
276
+ const config = loadConfig(run.cwd);
277
+ return labelsSync({ repo: repoOf(flags.repo, config, run.cwd), labels: config.labels, update: flags.update ?? false, dryRun: flags["dry-run"] ?? false, json: flags.json ?? false }, run.out);
278
+ }
256
279
  /**
257
280
  * Runs the `issue`, `worktree` and `session` commands.
258
281
  * @param group - The first word.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @file `issue-flow labels sync [--update] [--dry-run] [--json]`: makes in GitHub the labels
3
+ * of `labels` in `.agents/flow.json` that the repo lacks. Names compare ignoring case, as
4
+ * GitHub does, so `Bug` counts as `bug` and is never renamed. A label that exists with
5
+ * another color or description is only reported, unless `--update`. It never deletes. A
6
+ * write that fails does not stop the rest: every label is reported, and it exits 1.
7
+ */
8
+ import { type Label } from "@ramon-sg/github-rest";
9
+ import type { LabelSpec } from "../config.js";
10
+ import { type Output } from "../output.js";
11
+ /**
12
+ * What happened to one label: with `--dry-run`, `to-create` and `to-update` instead of
13
+ * `created` and `updated`; `failed` when GitHub refused the write.
14
+ */
15
+ export type LabelStatus = "created" | "updated" | "ok" | "differs" | "to-create" | "to-update" | "failed";
16
+ /** One label of the result. */
17
+ export interface LabelResult {
18
+ name: string;
19
+ status: LabelStatus;
20
+ color: string;
21
+ description: string;
22
+ /** What GitHub has, when it differs. */
23
+ found?: {
24
+ color: string;
25
+ description: string;
26
+ };
27
+ /** Why the write failed. */
28
+ error?: string;
29
+ }
30
+ /** The options of `labels sync`. */
31
+ export interface LabelsSyncOptions {
32
+ repo: string;
33
+ labels: Record<string, LabelSpec>;
34
+ update: boolean;
35
+ dryRun: boolean;
36
+ json: boolean;
37
+ }
38
+ /**
39
+ * What to do with each label, before writing anything.
40
+ * @param wanted - The labels of the config.
41
+ * @param existing - The labels in GitHub.
42
+ * @param update - Whether to fix the ones that differ.
43
+ * @returns Each label with its status, as if every write worked, and its name in GitHub if it exists.
44
+ */
45
+ export declare function planLabels(wanted: Record<string, LabelSpec>, existing: Label[], update: boolean): LabelResult[];
46
+ /**
47
+ * Runs `labels sync`; see the file's header.
48
+ * @param options - The repo, the labels and the flags.
49
+ * @param out - Where to write.
50
+ * @returns 0 when done; 1 if GitHub's list came back cut short (nothing is written then) or a write failed.
51
+ * @throws GitHubError if GitHub fails to list the labels.
52
+ * @example await labelsSync({ repo: "acme/widgets", labels, update: false, dryRun: true, json: false }, out)
53
+ */
54
+ export declare function labelsSync(options: LabelsSyncOptions, out: Output): Promise<number>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @file `issue-flow labels sync [--update] [--dry-run] [--json]`: makes in GitHub the labels
3
+ * of `labels` in `.agents/flow.json` that the repo lacks. Names compare ignoring case, as
4
+ * GitHub does, so `Bug` counts as `bug` and is never renamed. A label that exists with
5
+ * another color or description is only reported, unless `--update`. It never deletes. A
6
+ * write that fails does not stop the rest: every label is reported, and it exits 1.
7
+ */
8
+ import { createLabel, listLabels } from "@ramon-sg/github-rest";
9
+ import { emitJson } from "../output.js";
10
+ /** Width of the status column in the text output. */
11
+ const STATUS_WIDTH = 10;
12
+ /**
13
+ * Whether a label in GitHub has the wanted color and description (colors ignore case).
14
+ * @param found - The label in GitHub.
15
+ * @param spec - The wanted one.
16
+ * @returns True if they match.
17
+ */
18
+ function matches(found, spec) {
19
+ return found.color.toLowerCase() === spec.color.toLowerCase() && found.description === spec.description;
20
+ }
21
+ /**
22
+ * What to do with each label, before writing anything.
23
+ * @param wanted - The labels of the config.
24
+ * @param existing - The labels in GitHub.
25
+ * @param update - Whether to fix the ones that differ.
26
+ * @returns Each label with its status, as if every write worked, and its name in GitHub if it exists.
27
+ */
28
+ export function planLabels(wanted, existing, update) {
29
+ const byName = new Map(existing.map((label) => [label.name.toLowerCase(), label]));
30
+ return Object.entries(wanted).map(([name, spec]) => {
31
+ const found = byName.get(name.toLowerCase());
32
+ const base = { name, color: spec.color, description: spec.description };
33
+ if (found === undefined)
34
+ return { ...base, status: "created" };
35
+ if (matches(found, spec))
36
+ return { ...base, name: found.name, status: "ok" };
37
+ return { ...base, name: found.name, status: update ? "updated" : "differs", found: { color: found.color, description: found.description } };
38
+ });
39
+ }
40
+ /** The word of each status in the text output. */
41
+ const WORDS = {
42
+ created: "created",
43
+ updated: "updated",
44
+ ok: "ok",
45
+ differs: "differs",
46
+ "to-create": "to create",
47
+ "to-update": "to update",
48
+ failed: "failed",
49
+ };
50
+ /**
51
+ * One line of the text output.
52
+ * @param result - The label.
53
+ * @returns The line.
54
+ */
55
+ function line(result) {
56
+ const head = `${WORDS[result.status].padEnd(STATUS_WIDTH)}${result.name}`;
57
+ if (result.error !== undefined)
58
+ return `${head}: ${result.error}`;
59
+ if (result.found === undefined)
60
+ return head;
61
+ const was = `${result.found.color} "${result.found.description}"`;
62
+ const wanted = `${result.color} "${result.description}"`;
63
+ return result.status === "differs" ? `${head}: has ${was}, the flow wants ${wanted} (--update fixes it)` : `${head}: ${was} → ${wanted}`;
64
+ }
65
+ /**
66
+ * Writes one label and says how it went. A label that appeared in GitHub after the list
67
+ * was read fails without `--update`, with a message that says to run it again.
68
+ * @param repo - `owner/repo`.
69
+ * @param result - The label, as planned.
70
+ * @returns The label with its final status.
71
+ */
72
+ async function write(repo, result) {
73
+ try {
74
+ await createLabel(repo, { name: result.name, color: result.color, description: result.description, force: result.status === "updated" });
75
+ return result;
76
+ }
77
+ catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ const appeared = result.status === "created" && message.includes("already exists");
80
+ return { ...result, status: "failed", error: appeared ? "it appeared in GitHub meanwhile: run labels sync again" : message };
81
+ }
82
+ }
83
+ /**
84
+ * Runs `labels sync`; see the file's header.
85
+ * @param options - The repo, the labels and the flags.
86
+ * @param out - Where to write.
87
+ * @returns 0 when done; 1 if GitHub's list came back cut short (nothing is written then) or a write failed.
88
+ * @throws GitHubError if GitHub fails to list the labels.
89
+ * @example await labelsSync({ repo: "acme/widgets", labels, update: false, dryRun: true, json: false }, out)
90
+ */
91
+ export async function labelsSync(options, out) {
92
+ const { labels: existing, truncated } = await listLabels(options.repo);
93
+ if (truncated) {
94
+ out.stderr(`issue-flow: could not read every label of ${options.repo}; nothing written, so no label is made twice.\n`);
95
+ return 1;
96
+ }
97
+ const results = [];
98
+ for (const item of planLabels(options.labels, existing, options.update)) {
99
+ const writes = item.status === "created" || item.status === "updated";
100
+ // NOTE: one at a time, as GitHub asks of writes, to stay clear of its secondary rate limit.
101
+ if (!writes)
102
+ results.push(item);
103
+ else if (options.dryRun)
104
+ results.push({ ...item, status: item.status === "created" ? "to-create" : "to-update" });
105
+ else
106
+ results.push(await write(options.repo, item));
107
+ }
108
+ if (options.json)
109
+ emitJson(out, { repo: options.repo, dryRun: options.dryRun, labels: results });
110
+ else
111
+ out.stdout(`${results.map(line).join("\n")}\n`);
112
+ return results.some((result) => result.status === "failed") ? 1 : 0;
113
+ }
package/dist/config.d.ts CHANGED
@@ -6,6 +6,11 @@ export interface TypeRules {
6
6
  labels: Record<string, string>;
7
7
  titlePrefixes: Record<string, string>;
8
8
  }
9
+ /** A label the flow wants in the repo: its color (hex, no `#`) and description. */
10
+ export interface LabelSpec {
11
+ color: string;
12
+ description: string;
13
+ }
9
14
  /** The whole config, with every default applied. */
10
15
  export interface FlowConfig {
11
16
  repo: string | null;
@@ -30,6 +35,7 @@ export interface FlowConfig {
30
35
  dirs: string[];
31
36
  };
32
37
  colors: Record<string, string>;
38
+ labels: Record<string, LabelSpec>;
33
39
  }
34
40
  /** The defaults, the same rules Abastio uses. */
35
41
  export declare const DEFAULT_CONFIG: FlowConfig;
package/dist/config.js CHANGED
@@ -49,6 +49,16 @@ export const DEFAULT_CONFIG = {
49
49
  "P3:": "yellow",
50
50
  "P4:": "cyan",
51
51
  },
52
+ labels: {
53
+ bug: { color: "d73a4a", description: "Something isn't working" },
54
+ enhancement: { color: "a2eeef", description: "New feature or request" },
55
+ documentation: { color: "0075ca", description: "Improvements or additions to documentation" },
56
+ spike: { color: "fbca04", description: "Time-boxed research before building" },
57
+ "P1: urgent": { color: "B60205", description: "Breaks production or blocks work" },
58
+ "P2: important": { color: "F9D0C4", description: "Real impact, with some slack" },
59
+ "P3: minor": { color: "FEF2C0", description: "Default when in doubt" },
60
+ "P4: low": { color: "BFDADC", description: "Nice-to-have" },
61
+ },
52
62
  };
53
63
  /** The colors a Claude session takes with `/color`. */
54
64
  export const SESSION_COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"];
@@ -171,6 +181,55 @@ function colorsSection(value) {
171
181
  }
172
182
  return value;
173
183
  }
184
+ /** A label color: six hex digits, without `#`. */
185
+ const LABEL_COLOR = /^[0-9a-fA-F]{6}$/;
186
+ /** A label name: no spaces at the ends, since GitHub trims them. */
187
+ const LABEL_NAME = /^\S(?:.*\S)?$/;
188
+ /** GitHub's limits for a label's name and description, in characters. */
189
+ const LABEL_NAME_MAX = 50;
190
+ const LABEL_DESCRIPTION_MAX = 100;
191
+ /**
192
+ * The `labels` map: the labels `labels sync` makes in the repo. Unlike the other maps, it
193
+ * merges over the default, because a repo usually adds labels: a name replaces the default
194
+ * of the same name (ignoring case, as GitHub does) and `null` drops it.
195
+ * @param value - The map, or undefined.
196
+ * @returns The labels.
197
+ * @throws ConfigError if a label is invalid.
198
+ * @example labelsSection({ spike: null, "area:api": { color: "1d76db" } })["area:api"].description // ""
199
+ */
200
+ function labelsSection(value) {
201
+ if (value === undefined)
202
+ return DEFAULT_CONFIG.labels;
203
+ if (!isObject(value))
204
+ throw new ConfigError(`${CONFIG_PATH}: "labels" must be an object.`);
205
+ // NOTE: a Map, so a "__proto__" key from JSON.parse stays a label and not the prototype.
206
+ const labels = new Map(Object.entries(DEFAULT_CONFIG.labels));
207
+ const seen = new Set();
208
+ for (const [name, spec] of Object.entries(value)) {
209
+ const where = `labels.${name}`;
210
+ if (!LABEL_NAME.test(name) || [...name].length > LABEL_NAME_MAX)
211
+ throw new ConfigError(`${CONFIG_PATH}: "${where}" needs a name of 1 to ${LABEL_NAME_MAX} characters, without spaces at the ends.`);
212
+ const key = name.toLowerCase();
213
+ if (seen.has(key))
214
+ throw new ConfigError(`${CONFIG_PATH}: "labels" has "${name}" twice (names ignore case).`);
215
+ seen.add(key);
216
+ for (const known of [...labels.keys()])
217
+ if (known.toLowerCase() === key)
218
+ labels.delete(known);
219
+ if (spec === null)
220
+ continue;
221
+ if (!isObject(spec))
222
+ throw new ConfigError(`${CONFIG_PATH}: "${where}" must be an object or null.`);
223
+ onlyKeys(spec, ["color", "description"], where);
224
+ if (typeof spec.color !== "string" || !LABEL_COLOR.test(spec.color))
225
+ throw new ConfigError(`${CONFIG_PATH}: "${where}.color" must be six hex digits, without #, such as "1d76db".`);
226
+ const description = spec.description ?? "";
227
+ if (typeof description !== "string" || [...description].length > LABEL_DESCRIPTION_MAX)
228
+ throw new ConfigError(`${CONFIG_PATH}: "${where}.description" must be a string of up to ${LABEL_DESCRIPTION_MAX} characters.`);
229
+ labels.set(name, { color: spec.color, description });
230
+ }
231
+ return Object.fromEntries(labels);
232
+ }
174
233
  /**
175
234
  * Validates a parsed config and fills in the defaults.
176
235
  * @param raw - The parsed JSON.
@@ -181,7 +240,7 @@ function colorsSection(value) {
181
240
  export function parseConfig(raw) {
182
241
  if (!isObject(raw))
183
242
  throw new ConfigError(`${CONFIG_PATH} must be a JSON object.`);
184
- onlyKeys(raw, ["repo", "baseBranch", "branch", "worktrees", "start", "plans", "colors"], "");
243
+ onlyKeys(raw, ["repo", "baseBranch", "branch", "worktrees", "start", "plans", "colors", "labels"], "");
185
244
  const repo = text(raw.repo, "repo", null);
186
245
  if (repo !== null && !REPO.test(repo))
187
246
  throw new ConfigError(`${CONFIG_PATH}: "repo" must be owner/repo.`);
@@ -193,6 +252,7 @@ export function parseConfig(raw) {
193
252
  start: textSection(raw.start, "start", DEFAULT_CONFIG.start),
194
253
  plans: plansSection(raw.plans),
195
254
  colors: colorsSection(raw.colors),
255
+ labels: labelsSection(raw.labels),
196
256
  };
197
257
  }
198
258
  /**
package/dist/usage.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * @file The usage text of the CLI, printed with `--help` and after a usage error.
3
3
  */
4
4
  /** Usage text. */
5
- export declare const USAGE = "Usage:\n issue-flow issue branch <n>... [--namer claude|title] [--json] [-R owner/repo]\n issue-flow issue branch --missing [--limit <n>] [--namer claude|title] [--dry-run | --yes] [--json] [-R owner/repo]\n issue-flow issue start <n> [--branch] [--dry-run | --yes] [--resume <branch> | --new] [--no-launch]\n [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --plan [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --cloud | --here | --teleport | --adopt [...]\n issue-flow worktree setup [--only <step>]\n issue-flow worktree teardown [--path <dir>]\n issue-flow worktree ports [--next]\n issue-flow worktree cleanup [--path <dir>]\n issue-flow worktree prune [--only <dir>] [--yes] [-R owner/repo]\n issue-flow pr worktree <n> [--no-setup] [-R owner/repo] [-- [<command>...]]\n issue-flow adapter setup [--only <step>] | issue [<url>] | archive [-R owner/repo]\n issue-flow session\n issue-flow hook worktree-create | worktree-remove\n issue-flow init [--yes] [--recipe <name>,...]\n issue-flow doctor [--json]\n issue-flow upgrade [--dry-run]\n\nThe slug comes from the issue's \"Slug de rama\" block, then from Claude, then from the title,\nand the branch from the repo's template (an older \"Rama sugerida\" block is used if it fits).\n--missing adds the slug block to every open issue without a block, after confirming.\n\nissue start makes the branch in a worktree (or, with --branch, in this checkout), the\nstart commit, the push, the draft PR and the assignment, after confirming, and opens\nClaude: in auto mode if the repo has claude-repo-guard, with your settings with\n--ask-permissions, and with no sandbox nor questions with --free. --plan prints the branch\nand the prompt as JSON and changes nothing.\n\n--cloud confirms here and opens a Claude cloud session that starts the issue there.\n--here, inside a cloud session, starts it in this checkout and opens nothing. --teleport\nbrings the cloud session noted in the PR to the worktree. --adopt starts it in the\nworktree another tool (Orca, Paseo) just made, without asking nor opening Claude.\n\nworktree runs the steps of \"worktrees\" in .agents/flow.json: setup prepares this worktree,\nteardown cleans it before it goes (steps, ports, Docker), ports prints or moves its block\nof ports, cleanup deletes the Docker of deleted worktrees, and prune deletes the worktrees\nwhose PR was merged or closed. session is the SessionStart hook of Claude Code.\n\npr worktree makes (or reuses) a worktree on a PR's own branch, to review and fix it; with\n--, it runs the command there (Claude if none). adapter is what Orca and Paseo run: setup\nwhen they make a worktree (its steps and, if it came from an issue, issue start --adopt),\nissue to start one by hand, and archive before they delete it. hook implements Claude\nCode's WorktreeCreate and WorktreeRemove for claude --worktree.\n\ninit sets the flow up in the repo: .agents/flow.json (with the recipes its files suggest),\nthe dev dependencies and scripts, and the hooks. doctor checks them at this version, and\nupgrade moves them to this version. npx agent-kitchen runs the three, along with the\nskills, claude-cloud and claude-repo-guard.";
5
+ export declare const USAGE = "Usage:\n issue-flow issue branch <n>... [--namer claude|title] [--json] [-R owner/repo]\n issue-flow issue branch --missing [--limit <n>] [--namer claude|title] [--dry-run | --yes] [--json] [-R owner/repo]\n issue-flow issue start <n> [--branch] [--dry-run | --yes] [--resume <branch> | --new] [--no-launch]\n [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --plan [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --cloud | --here | --teleport | --adopt [...]\n issue-flow worktree setup [--only <step>]\n issue-flow worktree teardown [--path <dir>]\n issue-flow worktree ports [--next]\n issue-flow worktree cleanup [--path <dir>]\n issue-flow worktree prune [--only <dir>] [--yes] [-R owner/repo]\n issue-flow pr worktree <n> [--no-setup] [-R owner/repo] [-- [<command>...]]\n issue-flow adapter setup [--only <step>] | issue [<url>] | archive [-R owner/repo]\n issue-flow session\n issue-flow hook worktree-create | worktree-remove\n issue-flow init [--yes] [--recipe <name>,...]\n issue-flow doctor [--json]\n issue-flow upgrade [--dry-run]\n issue-flow labels sync [--update] [--dry-run] [--json] [-R owner/repo]\n\nThe slug comes from the issue's \"Slug de rama\" block, then from Claude, then from the title,\nand the branch from the repo's template (an older \"Rama sugerida\" block is used if it fits).\n--missing adds the slug block to every open issue without a block, after confirming.\n\nissue start makes the branch in a worktree (or, with --branch, in this checkout), the\nstart commit, the push, the draft PR and the assignment, after confirming, and opens\nClaude: in auto mode if the repo has claude-repo-guard, with your settings with\n--ask-permissions, and with no sandbox nor questions with --free. --plan prints the branch\nand the prompt as JSON and changes nothing.\n\n--cloud confirms here and opens a Claude cloud session that starts the issue there.\n--here, inside a cloud session, starts it in this checkout and opens nothing. --teleport\nbrings the cloud session noted in the PR to the worktree. --adopt starts it in the\nworktree another tool (Orca, Paseo) just made, without asking nor opening Claude.\n\nworktree runs the steps of \"worktrees\" in .agents/flow.json: setup prepares this worktree,\nteardown cleans it before it goes (steps, ports, Docker), ports prints or moves its block\nof ports, cleanup deletes the Docker of deleted worktrees, and prune deletes the worktrees\nwhose PR was merged or closed. session is the SessionStart hook of Claude Code.\n\npr worktree makes (or reuses) a worktree on a PR's own branch, to review and fix it; with\n--, it runs the command there (Claude if none). adapter is what Orca and Paseo run: setup\nwhen they make a worktree (its steps and, if it came from an issue, issue start --adopt),\nissue to start one by hand, and archive before they delete it. hook implements Claude\nCode's WorktreeCreate and WorktreeRemove for claude --worktree.\n\ninit sets the flow up in the repo: .agents/flow.json (with the recipes its files suggest),\nthe dev dependencies and scripts, and the hooks. doctor checks them at this version, and\nupgrade moves them to this version. npx agent-kitchen runs the three, along with the\nskills, claude-cloud and claude-repo-guard.\n\nlabels sync makes in GitHub the labels the flow wants (\"labels\" in .agents/flow.json: the\nissue types and P1 to P4 by default) that the repo lacks. Names ignore case and are never\nrenamed nor deleted; --update also fixes the color and description of those that differ.";
package/dist/usage.js CHANGED
@@ -21,6 +21,7 @@ export const USAGE = `Usage:
21
21
  issue-flow init [--yes] [--recipe <name>,...]
22
22
  issue-flow doctor [--json]
23
23
  issue-flow upgrade [--dry-run]
24
+ issue-flow labels sync [--update] [--dry-run] [--json] [-R owner/repo]
24
25
 
25
26
  The slug comes from the issue's "Slug de rama" block, then from Claude, then from the title,
26
27
  and the branch from the repo's template (an older "Rama sugerida" block is used if it fits).
@@ -51,4 +52,8 @@ Code's WorktreeCreate and WorktreeRemove for claude --worktree.
51
52
  init sets the flow up in the repo: .agents/flow.json (with the recipes its files suggest),
52
53
  the dev dependencies and scripts, and the hooks. doctor checks them at this version, and
53
54
  upgrade moves them to this version. npx agent-kitchen runs the three, along with the
54
- skills, claude-cloud and claude-repo-guard.`;
55
+ skills, claude-cloud and claude-repo-guard.
56
+
57
+ labels sync makes in GitHub the labels the flow wants ("labels" in .agents/flow.json: the
58
+ issue types and P1 to P4 by default) that the repo lacks. Names ignore case and are never
59
+ renamed nor deleted; --update also fixes the color and description of those that differ.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramon-sg/issue-flow",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Issue-to-PR workflow for any repo: names each issue's branch, creates its worktree, opens the draft PR and launches Claude Code, locally and in Claude's cloud.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/schema.json CHANGED
@@ -267,6 +267,50 @@
267
267
  "P3:": "yellow",
268
268
  "P4:": "cyan"
269
269
  }
270
+ },
271
+ "labels": {
272
+ "description": "Labels that `issue-flow labels sync` makes in GitHub when the repo lacks them. Merges over the default: a name replaces the default of the same name (ignoring case) and null drops it.",
273
+ "type": "object",
274
+ "propertyNames": {
275
+ "pattern": "^\\S(?:.*\\S)?$",
276
+ "maxLength": 50
277
+ },
278
+ "additionalProperties": {
279
+ "oneOf": [
280
+ {
281
+ "type": "null"
282
+ },
283
+ {
284
+ "type": "object",
285
+ "additionalProperties": false,
286
+ "required": [
287
+ "color"
288
+ ],
289
+ "properties": {
290
+ "color": {
291
+ "description": "Six hex digits, without #.",
292
+ "type": "string",
293
+ "pattern": "^[0-9a-fA-F]{6}$"
294
+ },
295
+ "description": {
296
+ "type": "string",
297
+ "maxLength": 100,
298
+ "default": ""
299
+ }
300
+ }
301
+ }
302
+ ]
303
+ },
304
+ "default": {
305
+ "bug": { "color": "d73a4a", "description": "Something isn't working" },
306
+ "enhancement": { "color": "a2eeef", "description": "New feature or request" },
307
+ "documentation": { "color": "0075ca", "description": "Improvements or additions to documentation" },
308
+ "spike": { "color": "fbca04", "description": "Time-boxed research before building" },
309
+ "P1: urgent": { "color": "B60205", "description": "Breaks production or blocks work" },
310
+ "P2: important": { "color": "F9D0C4", "description": "Real impact, with some slack" },
311
+ "P3: minor": { "color": "FEF2C0", "description": "Default when in doubt" },
312
+ "P4: low": { "color": "BFDADC", "description": "Nice-to-have" }
313
+ }
270
314
  }
271
315
  },
272
316
  "$defs": {