@ramon-sg/issue-flow 0.3.3 → 0.3.4

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
@@ -119,7 +119,7 @@ Lo propio de cada repo (instalar, armar la base, levantar Docker) son **pasos**
119
119
  ```
120
120
 
121
121
  - **Cuándo corren:** `issue start` corre el `setup` en cada worktree nuevo, después de copiar `.worktreeinclude`. El hook `session` lo corre al abrir Claude en un worktree. El `teardown` corre antes de borrar un worktree, con `prune` o al limpiar después del merge.
122
- - **De dónde salen:** siempre de la config del checkout principal, nunca de la del worktree. Así una rama no puede sumar comandos a su propio setup. En la nube no hay otro checkout: ahí la config es la de la rama de la sesión.
122
+ - **De dónde salen:** del `.agents/flow.json` de `origin/<base>`, nunca del worktree. Así una rama no puede sumar comandos a su propio setup, y un checkout principal que quedó atrás no deja al worktree sin su `install`. Si los pasos del checkout principal son otros, `issue-flow` lo avisa. Sin `origin/<base>`, o si ahí el archivo no es válido, usa los del checkout principal. Sin pasos, `worktree setup` e `issue start` dicen de qué archivo los leyeron. En la nube no hay otro checkout: ahí la config es la de la rama de la sesión. El teardown sigue usando la config del checkout principal.
123
123
  - **Pueden correr más de una vez:** `issue start` y el hook `session` corren el `setup` del mismo worktree, así que un paso sin `once` tiene que poder repetirse.
124
124
  - **`trusted`:** el paso corre solo si el código del worktree está en `origin/<base>` y no tiene cambios. Es para un install, que corre los scripts de `package.json` de esa rama. En la nube corre siempre, porque la máquina es descartable.
125
125
  - **`once`:** el paso no se repite una vez que funcionó (deja una marca en el git dir del worktree).
@@ -22,11 +22,11 @@ import { canonicalPath, dockerCleanup } from "../docker.js";
22
22
  import { checkoutRoot, currentBranch, locateRepo } from "../git.js";
23
23
  import { issueOfBranch } from "../issue-text.js";
24
24
  import { inPath } from "../launch.js";
25
- import { UsageError, warn } from "../output.js";
25
+ import { warn } from "../output.js";
26
26
  import { releasePortClaims } from "../ports.js";
27
27
  import { copyWorktreeIncludes } from "../start-place.js";
28
28
  import { StartError } from "../start-state.js";
29
- import { runSetup, teardownWorktree } from "../worktree-steps.js";
29
+ import { checkOnly, runSetup, teardownWorktree } from "../worktree-steps.js";
30
30
  import { issueStart } from "./issue-start.js";
31
31
  import { stepPlace } from "./worktree.js";
32
32
  /** Orca's CLI, which knows which issue a workspace came from; `ISSUE_FLOW_ORCA_CLI` changes it. */
@@ -162,9 +162,7 @@ function linkedPlace(options, out) {
162
162
  * @example await adapterSetup(options, out)
163
163
  */
164
164
  export async function adapterSetup(options, out) {
165
- const names = options.config.worktrees.setup.map((step) => step.name);
166
- if (options.only !== null && !names.includes(options.only))
167
- throw new UsageError(`--only expects a setup step of .agents/flow.json (${names.join(", ") || "it has none"}): ${options.only}`);
165
+ checkOnly(stepPlace(options, out), options.only);
168
166
  const place = linkedPlace(options, out);
169
167
  if (place === null)
170
168
  return 0;
@@ -111,7 +111,7 @@ export async function worktreeCreate(input, config, cwd, out) {
111
111
  throw new StartError(`could not make the worktree ${path}: ${add.err}`);
112
112
  copyWorktreeIncludes(mainRoot, path, stderrOnly);
113
113
  }
114
- await runSetup({ root: path, mainRoot, base, config, out: stderrOnly, remote: false }, null);
114
+ await runSetup({ root: path, mainRoot, base, config, out: stderrOnly, remote: false }, null, true);
115
115
  out.stdout(`${path}\n`);
116
116
  return 0;
117
117
  }
@@ -90,7 +90,7 @@ export async function session(options, out) {
90
90
  if (remote)
91
91
  cloudSession(root, say);
92
92
  if (root !== mainRoot || remote)
93
- await runSetup(place, null);
93
+ await runSetup(place, null, true);
94
94
  const unstarted = !remote && root !== mainRoot ? unstartedIssue(root) : null;
95
95
  if (unstarted !== null)
96
96
  say(`issue #${unstarted} was not started: its branch is not published and has no draft PR. Before working, run the start-issue skill (${unstarted}).`);
@@ -12,11 +12,11 @@ import { resolve } from "node:path";
12
12
  import { canonicalPath, dockerCleanup } from "../docker.js";
13
13
  import { checkoutRoot, git, isClean, locateRepo, safeToDelete, worktrees } from "../git.js";
14
14
  import { forgetSession } from "../launch.js";
15
- import { UsageError, warn } from "../output.js";
15
+ import { warn } from "../output.js";
16
16
  import { claimPortBase, nextPortBase, portBase } from "../ports.js";
17
17
  import { isStale, LOCK_REASON } from "../session-lock.js";
18
18
  import { StartError } from "../start-state.js";
19
- import { runSetup, teardownWorktree } from "../worktree-steps.js";
19
+ import { checkOnly, runSetup, teardownWorktree } from "../worktree-steps.js";
20
20
  /** The reason `claude --worktree` locks the worktrees it makes while its session lives. */
21
21
  const CLAUDE_LOCK_REASON = "claude session";
22
22
  /**
@@ -57,10 +57,9 @@ export function stepPlace(options, out) {
57
57
  * @example await worktreeSetup(options, out)
58
58
  */
59
59
  export async function worktreeSetup(options, out) {
60
- const names = options.config.worktrees.setup.map((step) => step.name);
61
- if (options.only !== null && !names.includes(options.only))
62
- throw new UsageError(`--only expects a setup step of .agents/flow.json (${names.join(", ") || "it has none"}): ${options.only}`);
63
- return (await runSetup(stepPlace(options, out), options.only)) ? 0 : 1;
60
+ const place = stepPlace(options, out);
61
+ checkOnly(place, options.only);
62
+ return (await runSetup(place, options.only)) ? 0 : 1;
64
63
  }
65
64
  /**
66
65
  * `worktree teardown [--path <dir>]`: cleans a worktree before deleting it (its steps, its
@@ -1,5 +1,6 @@
1
- import type { FlowConfig } from "./config.js";
1
+ import { type FlowConfig } from "./config.js";
2
2
  import { type Output } from "./output.js";
3
+ import { type Step } from "./worktree-config.js";
3
4
  /** Folder, in the worktree's git dir, with a mark per `once` step that worked. */
4
5
  export declare const SETUP_MARKS_DIR = "issue-flow-setup";
5
6
  /** Where the steps run, and with what. */
@@ -30,17 +31,43 @@ export declare function trustedCode(root: string, base: string): boolean;
30
31
  * @example stepEnv(place, 21430).ISSUE_FLOW_PORT_BASE // "21430"
31
32
  */
32
33
  export declare function stepEnv(place: StepPlace, port: number | null): Record<string, string>;
34
+ /** The setup steps a worktree runs, and where they were read. */
35
+ export interface SetupSteps {
36
+ steps: Step[];
37
+ source: string;
38
+ warning: string | null;
39
+ }
40
+ /**
41
+ * The setup steps: those of `.agents/flow.json` in `origin/<base>`, which a worktree made
42
+ * from it has; none if the file is not there. In a cloud session, without `origin/<base>`,
43
+ * or with a file there that is not valid, those of the checkout's config. The warning says
44
+ * when the main checkout's steps differ, or why they were used.
45
+ * @param place - Where.
46
+ * @returns The steps, where they come from and a warning, or null.
47
+ * @example setupSteps(place).source // "origin/main:.agents/flow.json"
48
+ */
49
+ export declare function setupSteps(place: StepPlace): SetupSteps;
50
+ /**
51
+ * Checks that `--only` names one of the setup steps.
52
+ * @param place - Where.
53
+ * @param only - The step, or null.
54
+ * @throws UsageError if it names no step.
55
+ * @example checkOnly(place, "deps")
56
+ */
57
+ export declare function checkOnly(place: StepPlace, only: string | null): void;
33
58
  /**
34
59
  * Prepares a worktree with the setup steps: one (`only`) or all, in order. In the main
35
60
  * checkout it does nothing, except in a cloud session, whose checkout is not a worktree. It
36
61
  * fixes the block of ports first, so the steps (and the apps after them) agree on it.
37
- * Never stops on a failed step: it says what to run and goes on.
62
+ * Never stops on a failed step: it says what to run and goes on. Without steps it says so,
63
+ * unless `quiet` (the session hooks, which run at every start).
38
64
  * @param place - Where.
39
65
  * @param only - The step to run, or null for all.
66
+ * @param quiet - True to say nothing when there are no steps.
40
67
  * @returns True if every step that ran worked.
41
68
  * @example await runSetup(place, null)
42
69
  */
43
- export declare function runSetup(place: StepPlace, only: string | null): Promise<boolean>;
70
+ export declare function runSetup(place: StepPlace, only: string | null, quiet?: boolean): Promise<boolean>;
44
71
  /**
45
72
  * Cleans a worktree before it is deleted: its apps and then its teardown steps (if the
46
73
  * folder still exists), so a step that drops a database finds nobody connected; then its
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * @file Runs the steps of `worktrees.setup` and `worktrees.teardown` in a worktree. The
3
- * steps come from the main checkout's config, not the worktree's, so a branch cannot add
4
- * commands to its own setup. A `trusted` step (an install, which runs the lifecycle scripts
3
+ * setup steps come from `.agents/flow.json` in `origin/<base>`, not from the worktree, so a
4
+ * branch cannot add commands to its own setup, and a main checkout nobody pulled does not
5
+ * leave a new worktree without its install. The teardown steps come from the main
6
+ * checkout's config. A `trusted` step (an install, which runs the lifecycle scripts
5
7
  * of `package.json`) runs only if the worktree's code is in `origin/<base>` and has no
6
8
  * changes; a `once` step leaves a mark when it works and is not repeated. Two setups of one
7
9
  * worktree at a time (the tool's and the session hook's) take turns with a lock. Each step
@@ -12,11 +14,14 @@ import { spawnSync } from "node:child_process";
12
14
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
13
15
  import { basename, join } from "node:path";
14
16
  import { setTimeout as sleep } from "node:timers/promises";
17
+ import { CONFIG_PATH, isObject } from "./config-base.js";
18
+ import { DEFAULT_CONFIG } from "./config.js";
15
19
  import { dockerCleanup, sanitizeLabel } from "./docker.js";
16
20
  import { git, isClean } from "./git.js";
17
- import { warn } from "./output.js";
21
+ import { UsageError, warn } from "./output.js";
18
22
  import { claimPortBase, portBase, releasePortClaims, sha256 } from "./ports.js";
19
23
  import { stopApps } from "./stop-apps.js";
24
+ import { worktreesSection } from "./worktree-config.js";
20
25
  /** Folder, in the worktree's git dir, with a mark per `once` step that worked. */
21
26
  export const SETUP_MARKS_DIR = "issue-flow-setup";
22
27
  /** The lock of the setup, in the worktree's git dir. */
@@ -78,6 +83,58 @@ function runStep(place, step, env) {
78
83
  warn(place.out, `step "${step.name}" failed${result.status === null ? "" : ` (exit ${result.status})`}; run '${step.run}' in ${place.root}.`);
79
84
  return false;
80
85
  }
86
+ /**
87
+ * The setup steps: those of `.agents/flow.json` in `origin/<base>`, which a worktree made
88
+ * from it has; none if the file is not there. In a cloud session, without `origin/<base>`,
89
+ * or with a file there that is not valid, those of the checkout's config. The warning says
90
+ * when the main checkout's steps differ, or why they were used.
91
+ * @param place - Where.
92
+ * @returns The steps, where they come from and a warning, or null.
93
+ * @example setupSteps(place).source // "origin/main:.agents/flow.json"
94
+ */
95
+ export function setupSteps(place) {
96
+ const local = place.config.worktrees.setup;
97
+ const ref = `origin/${place.base}`;
98
+ const fallback = { steps: local, source: `the main checkout's ${CONFIG_PATH}` };
99
+ // NOTE: a cloud session has no other checkout: its steps are those of the session's branch.
100
+ if (place.remote || !git(place.mainRoot, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]).ok)
101
+ return { ...fallback, warning: null };
102
+ const source = `${ref}:${CONFIG_PATH}`;
103
+ const shown = git(place.mainRoot, ["show", source]);
104
+ let steps;
105
+ try {
106
+ if (!shown.ok)
107
+ steps = DEFAULT_CONFIG.worktrees.setup;
108
+ else {
109
+ const raw = JSON.parse(shown.out);
110
+ steps = worktreesSection(isObject(raw) ? raw.worktrees : undefined, DEFAULT_CONFIG.worktrees).setup;
111
+ }
112
+ }
113
+ catch (error) {
114
+ const reason = error instanceof Error ? error.message : String(error);
115
+ return { ...fallback, warning: `${source} is not valid: ${reason}; using the main checkout's steps.` };
116
+ }
117
+ const differ = JSON.stringify(steps) !== JSON.stringify(local);
118
+ return {
119
+ steps,
120
+ source,
121
+ warning: differ ? `the setup steps come from ${source}; the main checkout's differ (is it behind? pull it).` : null,
122
+ };
123
+ }
124
+ /**
125
+ * Checks that `--only` names one of the setup steps.
126
+ * @param place - Where.
127
+ * @param only - The step, or null.
128
+ * @throws UsageError if it names no step.
129
+ * @example checkOnly(place, "deps")
130
+ */
131
+ export function checkOnly(place, only) {
132
+ if (only === null)
133
+ return;
134
+ const names = setupSteps(place).steps.map((step) => step.name);
135
+ if (!names.includes(only))
136
+ throw new UsageError(`--only expects a setup step of ${CONFIG_PATH} (${names.join(", ") || "it has none"}): ${only}`);
137
+ }
81
138
  /**
82
139
  * Takes the setup's lock: a folder with the pid. A lock of a process that is gone is taken
83
140
  * over, and so is one without a pid that is some seconds old (its process died before
@@ -126,17 +183,24 @@ function alive(pid) {
126
183
  * Prepares a worktree with the setup steps: one (`only`) or all, in order. In the main
127
184
  * checkout it does nothing, except in a cloud session, whose checkout is not a worktree. It
128
185
  * fixes the block of ports first, so the steps (and the apps after them) agree on it.
129
- * Never stops on a failed step: it says what to run and goes on.
186
+ * Never stops on a failed step: it says what to run and goes on. Without steps it says so,
187
+ * unless `quiet` (the session hooks, which run at every start).
130
188
  * @param place - Where.
131
189
  * @param only - The step to run, or null for all.
190
+ * @param quiet - True to say nothing when there are no steps.
132
191
  * @returns True if every step that ran worked.
133
192
  * @example await runSetup(place, null)
134
193
  */
135
- export async function runSetup(place, only) {
194
+ export async function runSetup(place, only, quiet = false) {
136
195
  const { root, mainRoot, config, out } = place;
137
196
  if (root === mainRoot && !place.remote)
138
197
  return true;
139
- const chosen = config.worktrees.setup.filter((step) => only === null || step.name === only);
198
+ const found = setupSteps(place);
199
+ if (found.warning !== null)
200
+ warn(out, found.warning);
201
+ if (found.steps.length === 0 && !quiet)
202
+ warn(out, `no setup steps in ${found.source}; nothing ran.`);
203
+ const chosen = found.steps.filter((step) => only === null || step.name === only);
140
204
  const ports = config.worktrees.ports;
141
205
  const port = ports === null ? null : await claimPortBase(ports, root, null);
142
206
  if (ports !== null && port === null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramon-sg/issue-flow",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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
@@ -76,7 +76,7 @@
76
76
  "default": ".claude/worktrees"
77
77
  },
78
78
  "setup": {
79
- "description": "Steps that prepare a worktree, in order. They come from the main checkout's config, never the worktree's.",
79
+ "description": "Steps that prepare a worktree, in order. They come from this file in origin/<base> (the main checkout's if origin has none to read), never the worktree's.",
80
80
  "type": "array",
81
81
  "items": {
82
82
  "type": "object",