@ramon-sg/issue-flow 0.3.3 → 0.3.5
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 +2 -2
- package/dist/commands/adapter.js +3 -5
- package/dist/commands/claude-hooks.js +1 -1
- package/dist/commands/session.js +1 -1
- package/dist/commands/worktree.d.ts +1 -1
- package/dist/commands/worktree.js +12 -8
- package/dist/git.d.ts +12 -0
- package/dist/git.js +31 -0
- package/dist/start-place.d.ts +2 -2
- package/dist/start-place.js +6 -6
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +3 -1
- package/dist/worktree-steps.d.ts +30 -3
- package/dist/worktree-steps.js +70 -6
- package/package.json +1 -1
- package/schema.json +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ issue-flow worktree setup --only deps # uno solo
|
|
|
99
99
|
issue-flow worktree teardown # sus pasos de limpieza, sus puertos y su Docker
|
|
100
100
|
issue-flow worktree ports [--next] # el primer puerto de su bloque, o lo pasa al siguiente
|
|
101
101
|
issue-flow worktree cleanup # el Docker de los worktrees que ya no existen
|
|
102
|
-
issue-flow worktree prune # borra los worktrees cuyo PR se mergeó o se
|
|
102
|
+
issue-flow worktree prune # borra los worktrees cuyo PR se mergeó o se cerró, y pone al día la base local
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Lo propio de cada repo (instalar, armar la base, levantar Docker) son **pasos** en `worktrees` de `.agents/flow.json`; `issue-flow` solo los corre:
|
|
@@ -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:**
|
|
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).
|
package/dist/commands/adapter.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/session.js
CHANGED
|
@@ -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}).`);
|
|
@@ -72,7 +72,7 @@ export declare function worktreeCleanup(options: WorktreeOptions, out: Output):
|
|
|
72
72
|
* closed, after asking: their teardown, the folder, the local branch and the session id.
|
|
73
73
|
* It never touches one with uncommitted changes, unpushed commits, or a lock (a live
|
|
74
74
|
* session of `claude --worktree` or `issue start`, or by hand), and checks each again just
|
|
75
|
-
* before deleting it.
|
|
75
|
+
* before deleting it. If it deleted one, it moves the local base forward to origin's.
|
|
76
76
|
* @param options - The options.
|
|
77
77
|
* @param out - Where to write.
|
|
78
78
|
* @returns The exit code.
|
|
@@ -10,13 +10,13 @@ import { confirm } from "../ask.js";
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { canonicalPath, dockerCleanup } from "../docker.js";
|
|
13
|
-
import { checkoutRoot, git, isClean, locateRepo, safeToDelete, worktrees } from "../git.js";
|
|
13
|
+
import { checkoutRoot, git, isClean, locateRepo, safeToDelete, updateBase, worktrees } from "../git.js";
|
|
14
14
|
import { forgetSession } from "../launch.js";
|
|
15
|
-
import {
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
@@ -205,7 +204,7 @@ async function pruneCandidates(options, mainRoot, out) {
|
|
|
205
204
|
* closed, after asking: their teardown, the folder, the local branch and the session id.
|
|
206
205
|
* It never touches one with uncommitted changes, unpushed commits, or a lock (a live
|
|
207
206
|
* session of `claude --worktree` or `issue start`, or by hand), and checks each again just
|
|
208
|
-
* before deleting it.
|
|
207
|
+
* before deleting it. If it deleted one, it moves the local base forward to origin's.
|
|
209
208
|
* @param options - The options.
|
|
210
209
|
* @param out - Where to write.
|
|
211
210
|
* @returns The exit code.
|
|
@@ -225,6 +224,7 @@ export async function worktreePrune(options, out) {
|
|
|
225
224
|
return 0;
|
|
226
225
|
}
|
|
227
226
|
const { commonDir } = locateRepo(mainRoot) ?? { commonDir: "" };
|
|
227
|
+
let deleted = 0;
|
|
228
228
|
for (const item of doomed) {
|
|
229
229
|
if (!unchanged(mainRoot, item)) {
|
|
230
230
|
out.stdout(`changed since the check, not deleted: ${item.path}\n`);
|
|
@@ -239,6 +239,10 @@ export async function worktreePrune(options, out) {
|
|
|
239
239
|
warn(out, `could not delete the local branch ${item.branch}.`);
|
|
240
240
|
forgetSession(commonDir, item.branch);
|
|
241
241
|
out.stdout(`deleted: ${item.path} (${item.branch})\n`);
|
|
242
|
+
deleted += 1;
|
|
242
243
|
}
|
|
244
|
+
const stale = deleted === 0 ? null : updateBase(mainRoot, place.base);
|
|
245
|
+
if (stale !== null)
|
|
246
|
+
warn(out, stale);
|
|
243
247
|
return 0;
|
|
244
248
|
}
|
package/dist/git.d.ts
CHANGED
|
@@ -144,3 +144,15 @@ export declare function checkoutRoot(cwd: string): string | null;
|
|
|
144
144
|
* @example safeToDelete("/repo", "feat/42-x", "")
|
|
145
145
|
*/
|
|
146
146
|
export declare function safeToDelete(cwd: string, branch: string, head: string): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Brings `origin/<base>` and moves the local base forward to it, if nothing is lost. A base
|
|
149
|
+
* not checked out anywhere moves as a ref only; one checked out is merged there with
|
|
150
|
+
* `--ff-only`, and only if that checkout has no changes. A base with commits that are not in
|
|
151
|
+
* origin is left alone.
|
|
152
|
+
* @param mainRoot - The main checkout.
|
|
153
|
+
* @param base - The base branch.
|
|
154
|
+
* @returns Null if it is up to date now (or there is no local base); else why it was not
|
|
155
|
+
* moved.
|
|
156
|
+
* @example updateBase("/repo", "main") // null
|
|
157
|
+
*/
|
|
158
|
+
export declare function updateBase(mainRoot: string, base: string): string | null;
|
package/dist/git.js
CHANGED
|
@@ -213,3 +213,34 @@ export function safeToDelete(cwd, branch, head) {
|
|
|
213
213
|
return true;
|
|
214
214
|
return head !== "" && git(cwd, ["merge-base", "--is-ancestor", branch, head]).ok;
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Brings `origin/<base>` and moves the local base forward to it, if nothing is lost. A base
|
|
218
|
+
* not checked out anywhere moves as a ref only; one checked out is merged there with
|
|
219
|
+
* `--ff-only`, and only if that checkout has no changes. A base with commits that are not in
|
|
220
|
+
* origin is left alone.
|
|
221
|
+
* @param mainRoot - The main checkout.
|
|
222
|
+
* @param base - The base branch.
|
|
223
|
+
* @returns Null if it is up to date now (or there is no local base); else why it was not
|
|
224
|
+
* moved.
|
|
225
|
+
* @example updateBase("/repo", "main") // null
|
|
226
|
+
*/
|
|
227
|
+
export function updateBase(mainRoot, base) {
|
|
228
|
+
const remote = `refs/remotes/origin/${base}`;
|
|
229
|
+
const local = `refs/heads/${base}`;
|
|
230
|
+
if (!git(mainRoot, ["fetch", "-q", "origin", `+refs/heads/${base}:${remote}`]).ok)
|
|
231
|
+
return `could not fetch ${base} from origin; local ${base} was not updated.`;
|
|
232
|
+
if (!hasRef(mainRoot, local))
|
|
233
|
+
return null;
|
|
234
|
+
const from = git(mainRoot, ["rev-parse", local]).out;
|
|
235
|
+
const to = git(mainRoot, ["rev-parse", remote]).out;
|
|
236
|
+
if (from === to)
|
|
237
|
+
return null;
|
|
238
|
+
if (!git(mainRoot, ["merge-base", "--is-ancestor", local, remote]).ok)
|
|
239
|
+
return `local ${base} has commits that are not in origin/${base}; it was not updated.`;
|
|
240
|
+
const at = checkedOutAt(mainRoot, base);
|
|
241
|
+
if (at === null)
|
|
242
|
+
return git(mainRoot, ["update-ref", local, to, from]).ok ? null : `could not move local ${base} to origin/${base}.`;
|
|
243
|
+
if (!isClean(at, false))
|
|
244
|
+
return `${base} is checked out in ${at} with changes; it was not updated. Run 'git pull --ff-only' there.`;
|
|
245
|
+
return git(at, ["merge", "-q", "--ff-only", remote]).ok ? null : `could not update ${base} in ${at}; run 'git pull --ff-only' there.`;
|
|
246
|
+
}
|
package/dist/start-place.d.ts
CHANGED
|
@@ -52,8 +52,8 @@ export declare function publish(state: StartState): Promise<number>;
|
|
|
52
52
|
export declare function copyWorktreeIncludes(from: string, to: string, out: Output): void;
|
|
53
53
|
/**
|
|
54
54
|
* After Claude: if the PR was merged and nothing would be lost, offers to delete the
|
|
55
|
-
* worktree and the branch (or, in the main checkout, to go back to the base)
|
|
56
|
-
* says how to resume.
|
|
55
|
+
* worktree and the branch (or, in the main checkout, to go back to the base), and then
|
|
56
|
+
* moves the local base forward to origin's; if not, says how to resume.
|
|
57
57
|
* @param state - The start.
|
|
58
58
|
* @param lock - The worktree lock (shared: another session uses it).
|
|
59
59
|
* @example await afterSession(state, lock)
|
package/dist/start-place.js
CHANGED
|
@@ -9,7 +9,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:f
|
|
|
9
9
|
import { dirname, join, relative } from "node:path";
|
|
10
10
|
import { assign, createPr, findPr, getPr, updatePr } from "@ramon-sg/github-rest";
|
|
11
11
|
import { confirm } from "./ask.js";
|
|
12
|
-
import { checkedOutAt, currentBranch, git, gitLines, hasLocal, isClean, onOrigin, safeToDelete, trackOrigin, untracked } from "./git.js";
|
|
12
|
+
import { checkedOutAt, currentBranch, git, gitLines, hasLocal, isClean, onOrigin, safeToDelete, trackOrigin, untracked, updateBase } from "./git.js";
|
|
13
13
|
import { bodyToPartOf, dirName, fill, prBody } from "./issue-text.js";
|
|
14
14
|
import { forgetSession } from "./launch.js";
|
|
15
15
|
import { warn } from "./output.js";
|
|
@@ -262,8 +262,8 @@ function keepReason(state, lock, head) {
|
|
|
262
262
|
}
|
|
263
263
|
/**
|
|
264
264
|
* After Claude: if the PR was merged and nothing would be lost, offers to delete the
|
|
265
|
-
* worktree and the branch (or, in the main checkout, to go back to the base)
|
|
266
|
-
* says how to resume.
|
|
265
|
+
* worktree and the branch (or, in the main checkout, to go back to the base), and then
|
|
266
|
+
* moves the local base forward to origin's; if not, says how to resume.
|
|
267
267
|
* @param state - The start.
|
|
268
268
|
* @param lock - The worktree lock (shared: another session uses it).
|
|
269
269
|
* @example await afterSession(state, lock)
|
|
@@ -296,10 +296,10 @@ export async function afterSession(state, lock) {
|
|
|
296
296
|
if (!(await confirm(out, `The PR was merged. Go back to ${base} and delete the local branch ${branch}?`)))
|
|
297
297
|
return;
|
|
298
298
|
mustGit(mainRoot, ["checkout", "-q", base], `could not check out ${base}`);
|
|
299
|
-
const updated = git(mainRoot, ["fetch", "-q", "origin"]).ok && git(mainRoot, ["merge", "-q", "--ff-only", `origin/${base}`]).ok;
|
|
300
|
-
if (!updated)
|
|
301
|
-
warn(out, `could not update ${base}; do it by hand.`);
|
|
302
299
|
}
|
|
303
300
|
mustGit(mainRoot, ["branch", "-q", "-D", branch], `could not delete ${branch}`);
|
|
304
301
|
forgetSession(commonDir, branch);
|
|
302
|
+
const stale = updateBase(mainRoot, base);
|
|
303
|
+
if (stale !== null)
|
|
304
|
+
warn(out, stale);
|
|
305
305
|
}
|
package/dist/usage.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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 [--no-repo-deny]] [--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 apply [--yes] [--dry-run] [--json] [--recipe <name>,...]\n issue-flow doctor [--json]\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, with no sandbox nor questions with --free, and also without the repo's\ndeny rules with --free --no-repo-deny. --plan prints the branch and the prompt as JSON and\nchanges nothing. issue start --help explains each flag.\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\napply leaves the repo as this version needs it: .agents/flow.json (the first time, with\nthe recipes its files suggest), issue-flow at this version and github-rest and the scripts\nif missing in package.json, and the hooks; it changes no other version, and refuses a newer\nissue-flow or a range. doctor checks them. npx agent-kitchen apply runs apply at the version\nit picks, along with the skills, claude-cloud and claude-repo-guard. init and upgrade still\nrun apply, with a warning, until 0.4.0.\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.";
|
|
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 [--no-repo-deny]] [--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 apply [--yes] [--dry-run] [--json] [--recipe <name>,...]\n issue-flow doctor [--json]\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, with no sandbox nor questions with --free, and also without the repo's\ndeny rules with --free --no-repo-deny. --plan prints the branch and the prompt as JSON and\nchanges nothing. issue start --help explains each flag.\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, then moves the local base forward to origin's (only if\nnothing is lost: not with changes where it is checked out, nor with commits not in\norigin). 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\napply leaves the repo as this version needs it: .agents/flow.json (the first time, with\nthe recipes its files suggest), issue-flow at this version and github-rest and the scripts\nif missing in package.json, and the hooks; it changes no other version, and refuses a newer\nissue-flow or a range. doctor checks them. npx agent-kitchen apply runs apply at the version\nit picks, along with the skills, claude-cloud and claude-repo-guard. init and upgrade still\nrun apply, with a warning, until 0.4.0.\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.";
|
|
6
6
|
/** Usage text of `issue start`, printed with `issue start --help`. */
|
|
7
7
|
export declare const START_USAGE = "Usage: issue-flow issue start <n> [flags]\n\nStarts issue <n>: the branch (in a worktree, or in this checkout with --branch), the empty\nstart commit, the push, the draft PR and the assignment, after confirming; then it opens\nClaude there.\n\nFlags:\n --dry-run show what it would do and change nothing\n -y, --yes do it without asking\n --branch in this checkout instead of a worktree\n --resume <branch> take that branch of the issue\n --new make another branch, even if the issue has one\n --no-launch leave everything ready and do not open Claude\n --namer claude|title who names the branch when the issue has no slug block\n -R owner/repo another repo than the checkout's\n --plan print the branch and the prompt as JSON, and change nothing\n --cloud open a Claude cloud session that starts the issue there\n --here inside a cloud session, start it in this checkout\n --teleport bring the cloud session noted in the PR to the worktree\n --adopt start it in the worktree Orca or Paseo just made\n -h, --help this help\n\nPermissions of the Claude it opens (one at most):\n (none) auto mode if the repo has claude-repo-guard, else your settings\n --ask-permissions your settings: Claude asks before what they do not allow\n --free no sandbox and no questions, as claude:free\n --no-repo-deny with --free: also without the deny rules of the repo's settings\n\nWhat each one takes away, and what stays:\n (none), --ask-permissions\n take nothing away: the sandbox, the questions your settings ask\n and every deny rule stay.\n --free takes away the sandbox and the questions. Every deny rule stays:\n yours (~/.claude), the repo's (.claude/settings.json) and the\n local ones (.claude/settings.local.json).\n --free --no-repo-deny\n also takes the deny rules out of the worktree's\n .claude/settings.json while Claude runs, and puts them back when it\n exits; git does not see the change meanwhile. Your deny rules and the\n local ones stay, and so do the repo's hooks, attribution, CLAUDE.md,\n skills, agents and MCP servers. If Claude changes that file, the\n rules go back on top of its change, with a warning to review it. If\n the terminal closes first, the next issue start of the issue puts\n them back.\n\nFrom the repo's scripts (issue:start, and issue:start:free, which adds --free):\n npm run issue:start:free 42 npm passes a number on as it is\n npm run issue:start -- 42 --free but keeps the flags for itself, unless after --\n pnpm issue:start 42 --free pnpm, bun and yarn pass everything on\n bun run issue:start 42 --free\n yarn issue:start 42 --free\nA -- does no harm with any of them: issue-flow drops it.\n\nissue:start:free does not go with --ask-permissions, --cloud, --here nor --adopt: use\nissue:start for those. --no-repo-deny goes after it, as any flag:\n npm run issue:start:free -- 42 --no-repo-deny\n pnpm issue:start:free 42 --no-repo-deny";
|
|
8
8
|
/**
|
package/dist/usage.js
CHANGED
|
@@ -41,7 +41,9 @@ worktree another tool (Orca, Paseo) just made, without asking nor opening Claude
|
|
|
41
41
|
worktree runs the steps of "worktrees" in .agents/flow.json: setup prepares this worktree,
|
|
42
42
|
teardown cleans it before it goes (steps, ports, Docker), ports prints or moves its block
|
|
43
43
|
of ports, cleanup deletes the Docker of deleted worktrees, and prune deletes the worktrees
|
|
44
|
-
whose PR was merged or closed
|
|
44
|
+
whose PR was merged or closed, then moves the local base forward to origin's (only if
|
|
45
|
+
nothing is lost: not with changes where it is checked out, nor with commits not in
|
|
46
|
+
origin). session is the SessionStart hook of Claude Code.
|
|
45
47
|
|
|
46
48
|
pr worktree makes (or reuses) a worktree on a PR's own branch, to review and fix it; with
|
|
47
49
|
--, it runs the command there (Claude if none). adapter is what Orca and Paseo run: setup
|
package/dist/worktree-steps.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
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
|
package/dist/worktree-steps.js
CHANGED
|
@@ -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
|
|
4
|
-
* commands to its own setup
|
|
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
|
|
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
|
+
"version": "0.3.5",
|
|
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
|
|
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",
|