nightralph 0.0.1 → 0.0.3
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 +103 -52
- package/dist/index.js +80 -22
- package/dist/index.js.map +2 -2
- package/dist/merge.js +194 -0
- package/dist/merge.js.map +7 -0
- package/dist/meta.json +183 -17
- package/dist/orchestrator.js +459 -44
- package/dist/orchestrator.js.map +2 -2
- package/dist/progress.js +164 -0
- package/dist/progress.js.map +7 -0
- package/dist/resolve.js +16 -8
- package/dist/resolve.js.map +2 -2
- package/dist/setup.js +33 -5
- package/dist/setup.js.map +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/merge.d.ts +25 -0
- package/dist/src/merge.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +27 -3
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +27 -0
- package/dist/src/progress.d.ts.map +1 -0
- package/dist/src/resolve.d.ts +2 -1
- package/dist/src/resolve.d.ts.map +1 -1
- package/dist/src/setup.d.ts +1 -0
- package/dist/src/setup.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +18 -0
- package/dist/src/worktree.d.ts.map +1 -0
- package/dist/worktree.js +139 -0
- package/dist/worktree.js.map +7 -0
- package/package.json +8 -4
- package/.claude/settings.local.json +0 -5
- package/.github/dependabot.yml +0 -28
- package/.github/workflows/nodejs.yml +0 -30
- package/AGENTS.md +0 -60
- package/CLAUDE.md +0 -1
- package/PROMPT.md +0 -95
- package/WIP.md +0 -5
- package/dist/test/integration.test.js +0 -848
- package/dist/test/integration.test.js.map +0 -7
- package/dist/test/orchestrator.test.js +0 -749
- package/dist/test/orchestrator.test.js.map +0 -7
- package/dist/test/setup.test.js +0 -574
- package/dist/test/setup.test.js.map +0 -7
- package/docs/README.md +0 -12
- package/docs/agents/domain.md +0 -36
- package/docs/agents/issue-tracker.md +0 -30
- package/docs/agents/triage-labels.md +0 -15
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +0 -144
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +0 -292
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +0 -212
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +0 -203
- package/docs/skills.md +0 -58
- package/docs/test-plans/2026-08-27-issue-tracker-choice.md +0 -95
- package/eslint.config.js +0 -142
- package/lib.es5.d.ts +0 -19
- package/progress.log +0 -190
- package/ralph_claude.sh +0 -263
- package/scripts/clone-skills.sh +0 -52
- package/specs/prd.json +0 -172
- package/src/docs-templates/domain.md +0 -36
- package/src/docs-templates/issue-tracker-github.md +0 -228
- package/src/docs-templates/issue-tracker.md +0 -30
- package/src/docs-templates/triage-labels.md +0 -15
- package/src/index.ts +0 -184
- package/src/orchestrator.ts +0 -324
- package/src/resolve.ts +0 -73
- package/src/setup.ts +0 -157
- package/src/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/src/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/src/skills/domain-modeling/SKILL.md +0 -74
- package/src/skills/domain-modeling/agents/openai.yaml +0 -3
- package/src/skills/grill/SKILL.md +0 -7
- package/src/skills/grill/agents/openai.yaml +0 -5
- package/src/skills/grilling/SKILL.md +0 -28
- package/src/skills/grilling/agents/openai.yaml +0 -3
- package/src/skills/tdd/SKILL.md +0 -38
- package/src/skills/tdd/agents/openai.yaml +0 -3
- package/src/skills/tdd/mocking.md +0 -59
- package/src/skills/tdd/tests.md +0 -77
- package/src/skills/to-spec/SKILL.md +0 -75
- package/src/skills/to-spec/agents/openai.yaml +0 -5
- package/src/skills/to-tickets/SKILL.md +0 -105
- package/src/skills/to-tickets/agents/openai.yaml +0 -5
- package/src/skills/upstream.json +0 -5
- package/tasks/prd-integration-test-coverage.md +0 -243
- package/test/integration.test.ts +0 -636
- package/test/mock-agent-echo-args.js +0 -4
- package/test/mock-agent-fail.js +0 -3
- package/test/mock-agent-pass.js +0 -3
- package/test/orchestrator.test.ts +0 -609
- package/test/setup.test.ts +0 -417
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -31
package/README.md
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
This is a minimal Node-based AI orchestrator. It reads local markdown tickets
|
|
9
|
-
|
|
9
|
+
produced by
|
|
10
10
|
[`to-tickets` skill](https://github.com/mattpocock/skills/blob/main/skills/engineering/to-tickets/SKILL.md),
|
|
11
11
|
resolves their dependency graph, and runs agents in parallel as much as possible,
|
|
12
|
-
honoring the ticket dependencies.
|
|
12
|
+
honoring the ticket dependencies. During setup you can choose
|
|
13
|
+
a GitHub-oriented tracker, which installs skill instructions that use
|
|
14
|
+
`gh` CLI conventions, but the ticket source is always local markdown files.
|
|
13
15
|
|
|
14
16
|
This is inspired by [nightshift](https://github.com/Shaurya-Sethi/nightshift/).
|
|
15
|
-
I made it because I noticed
|
|
17
|
+
I made it because I noticed the API in `nightshift` had already drifted
|
|
16
18
|
from the skill definitions. This module bundles its skill dependencies,
|
|
17
19
|
so they can't drift as long as you install the skills with this package.
|
|
18
20
|
(See [the section on the `setup` command](#install-dependency-skills).)
|
|
@@ -23,9 +25,13 @@ so they can't drift as long as you install the skills with this package.
|
|
|
23
25
|
* [Install Dependency Skills](#install-dependency-skills)
|
|
24
26
|
- [Workflow](#workflow)
|
|
25
27
|
* [Create Issues](#create-issues)
|
|
26
|
-
* [Execute
|
|
27
|
-
- [
|
|
28
|
-
* [
|
|
28
|
+
* [Execute Tasks with `nightralph`](#execute-tasks-with-nightralph)
|
|
29
|
+
- [How It Works](#how-it-works)
|
|
30
|
+
* [Select a ticket](#select-a-ticket)
|
|
31
|
+
* [Worktrees](#worktrees)
|
|
32
|
+
* [Progress](#progress)
|
|
33
|
+
- [Keep Your Machine Awake](#keep-your-machine-awake)
|
|
34
|
+
* [Mac OS](#mac-os)
|
|
29
35
|
|
|
30
36
|
<!-- tocstop -->
|
|
31
37
|
|
|
@@ -37,7 +43,11 @@ npm i -S nightralph
|
|
|
37
43
|
|
|
38
44
|
### Install Dependency Skills
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
```sh
|
|
47
|
+
npx nightralph setup
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use the `setup` subcommand in this package to add the required
|
|
41
51
|
[Pocock skills](https://github.com/mattpocock/skills) to your repo.
|
|
42
52
|
This command will copy some files in this package to a specific directory
|
|
43
53
|
in your repo. The upstream
|
|
@@ -45,11 +55,6 @@ in your repo. The upstream
|
|
|
45
55
|
skill is exposed as `/grill`, along with its sub-skills `grilling`
|
|
46
56
|
and `domain-modeling`.
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
```sh
|
|
50
|
-
npx nightralph setup
|
|
51
|
-
```
|
|
52
|
-
|
|
53
58
|
The Pocock skills are bundled with this git repo, at `./skills`.
|
|
54
59
|
They are cloned with some metadata, visible in
|
|
55
60
|
[skill/upstream.json](skills/upstream.json).
|
|
@@ -58,22 +63,24 @@ That's how we prevent skill vs API drift.
|
|
|
58
63
|
|
|
59
64
|
## Workflow
|
|
60
65
|
|
|
66
|
+
**The expected flow is**
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
grill -> to-spec -> to-tickets -> tdd
|
|
70
|
+
```
|
|
71
|
+
|
|
61
72
|
First create issues.
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
skills to your local `./claude/skills/` folder or `./agents.skills/` folder.
|
|
74
|
+
After installing this package, use
|
|
75
|
+
[the `npx nightralph setup` command](#install-dependency-skills)
|
|
76
|
+
to add the required skills to your local `.agents/skills/` folder.
|
|
67
77
|
|
|
68
78
|
### Create Issues
|
|
69
79
|
|
|
70
|
-
Use the `to-tickets` skill to create issues in local markdown files
|
|
80
|
+
Use the `to-tickets` skill to create issues in local markdown files from
|
|
81
|
+
a prose text description.
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
grill -> to-spec -> to-tickets -> tdd
|
|
76
|
-
```
|
|
83
|
+
The `nightralph` CLI does the `tdd` part of the workflow.
|
|
77
84
|
|
|
78
85
|
>
|
|
79
86
|
> [!NOTE]
|
|
@@ -85,40 +92,49 @@ grill -> to-spec -> to-tickets -> tdd
|
|
|
85
92
|
> [!TIP]
|
|
86
93
|
> The `to-tickets` skill bundled here can use any input text
|
|
87
94
|
> describing a feature, meaning `grill` & `to-spec` skills are not required.
|
|
95
|
+
> You could, for example, use
|
|
96
|
+
> [superpowers:writing-plans](https://github.com/obra/superpowers/blob/main/skills/writing-plans/SKILL.md),
|
|
97
|
+
> then follow with `to-tickets`.
|
|
88
98
|
>
|
|
89
99
|
|
|
90
|
-
### Execute Issues
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
### Execute Tasks with `nightralph`
|
|
102
|
+
|
|
103
|
+
After you have the tickets, use the `nightralph` loop to execute.
|
|
104
|
+
The `to-tickets` skill produces markdown files at
|
|
105
|
+
`.scratch/<feature>/issues/<NN>-<slug>.md`, with task dependency info.
|
|
94
106
|
|
|
95
107
|
```bash
|
|
96
108
|
# Auto-discover .scratch features and run with claude
|
|
97
109
|
npx nightralph claude -m claude-opus-4-6
|
|
98
110
|
|
|
99
111
|
# Dry run to inspect the dependency graph and prompt
|
|
100
|
-
npx nightralph
|
|
112
|
+
npx nightralph codex --dry-run
|
|
101
113
|
|
|
102
|
-
#
|
|
103
|
-
npx nightralph claude -m claude-opus-4-6
|
|
104
|
-
--dir .scratch/my-feature/issues \
|
|
105
|
-
--spec .scratch/my-feature/spec.md
|
|
114
|
+
# Specify a feature by name
|
|
115
|
+
npx nightralph claude -m claude-opus-4-6 --spec my-feature
|
|
106
116
|
```
|
|
107
117
|
|
|
118
|
+
#### `--spec <name>` option
|
|
119
|
+
|
|
120
|
+
A feature name that resolves to `.scratch/<name>/`.
|
|
121
|
+
The spec file is `.scratch/<name>/spec.md` and the issues directory
|
|
122
|
+
is `.scratch/<name>/issues/`.
|
|
123
|
+
|
|
108
124
|
Flags:
|
|
109
125
|
|
|
110
126
|
| Flag | Required | Default | Description |
|
|
111
127
|
|------|----------|---------|-------------|
|
|
112
128
|
| `<provider>` | yes | -- | Agent CLI to spawn (e.g. `claude`, `codex`) |
|
|
113
129
|
| `-m`, `--model` | no | -- | Model flag passed to the agent |
|
|
114
|
-
| `--
|
|
115
|
-
| `--spec` | no | auto | Path to the spec file |
|
|
130
|
+
| `--spec` | no | auto | Feature name (resolves `.scratch/<name>/`) |
|
|
116
131
|
| `--dry-run` | no | -- | Show wave order and prompt without running |
|
|
117
|
-
|
|
|
132
|
+
| `-X` | no | -- | Stop on merge conflict instead of re-spawning |
|
|
133
|
+
| `--timeout` | no | `300` | Kill agent after N seconds |
|
|
118
134
|
|
|
119
135
|
Notes:
|
|
120
136
|
|
|
121
|
-
* When `--
|
|
137
|
+
* When `--spec` is omitted, the CLI looks for
|
|
122
138
|
`.scratch/<feature>/issues/` directories. If multiple features exist,
|
|
123
139
|
**it prompts you to pick one** with an interactive selector.
|
|
124
140
|
* Tickets are selected by `**Status:** ready-for-agent` in the file body.
|
|
@@ -126,34 +142,69 @@ Notes:
|
|
|
126
142
|
skips tickets whose blockers haven't completed.
|
|
127
143
|
* Unblocked tickets run in parallel. After a wave finishes, newly unblocked
|
|
128
144
|
tickets start in the next wave.
|
|
129
|
-
*
|
|
130
|
-
|
|
145
|
+
* An agent run only counts as a success when the agent exits 0,
|
|
146
|
+
its worktree branch has new commits relative to the base branch,
|
|
147
|
+
and the worktree is clean (no uncommitted changes). On success,
|
|
148
|
+
the ticket's status is updated to `done` in-place. If the agent
|
|
149
|
+
exits 0 but made no commits or left uncommitted changes, the
|
|
150
|
+
ticket is left `ready-for-agent` for a retry. Failed tickets
|
|
151
|
+
(non-zero exit) keep their status and block dependents.
|
|
131
152
|
* Agent logs are written to `<dir>/../logs/<ticket>.log`.
|
|
153
|
+
* Provider commands are launched non-interactively with default flags
|
|
154
|
+
that grant the agent write access to the worktree, so tickets can
|
|
155
|
+
actually be implemented without a permission prompt:
|
|
156
|
+
* `claude`: `--print --dangerously-skip-permissions`
|
|
157
|
+
* `codex`: `exec --full-auto`
|
|
132
158
|
|
|
133
159
|
|
|
134
|
-
##
|
|
160
|
+
## How It Works
|
|
135
161
|
|
|
136
|
-
|
|
162
|
+
All currently eligible tickets are grouped into a
|
|
163
|
+
"wave" and run in parallel. Each ticket gets a worktree named after the
|
|
164
|
+
ticket slug.
|
|
137
165
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
When the wave finishes, every agent that exited successfully has its ticket
|
|
167
|
+
marked `done`, which unlocks dependency tickets, which are grouped into the
|
|
168
|
+
next wave. This continues until all tickets are complete.
|
|
141
169
|
|
|
142
|
-
At that point you will have a number of tickets, which are markdown files
|
|
143
|
-
in your local repo. This tool handles the last step, implementing the tickets,
|
|
144
|
-
which normally is done with the `tdd` skill. This tool will work in a loop,
|
|
145
|
-
aiming to complete 1 task per session, then restarting (a classic Ralph loop).
|
|
146
170
|
|
|
147
|
-
###
|
|
171
|
+
### Select a ticket
|
|
148
172
|
|
|
149
|
-
|
|
173
|
+
* Build a ticket dependency graph with the `Blocked by:` fields
|
|
174
|
+
* A ticket is eligible for execution when its status is `ready-for-agent` and
|
|
175
|
+
every ticket listed in its `Blocked by:` field is completed.
|
|
150
176
|
|
|
151
|
-
```sh
|
|
152
|
-
npx nightralph claude -m claude-opus-4-6
|
|
153
|
-
```
|
|
154
177
|
|
|
155
|
-
|
|
178
|
+
### Worktrees
|
|
179
|
+
|
|
180
|
+
Each ticket gets its own git worktree. Agents work in
|
|
181
|
+
isolation so parallel tickets never conflict on disk. After a wave
|
|
182
|
+
completes, successful branches are merged back into the base branch.
|
|
183
|
+
The first branch merges directly, and subsequent branches rebase
|
|
184
|
+
onto the updated base before merging.
|
|
185
|
+
|
|
186
|
+
If a merge conflict occurs, the behavior depends on the `-X` flag: by
|
|
187
|
+
default the agent is re-spawned to resolve it; with `-X`,
|
|
188
|
+
the run stops immediately.
|
|
189
|
+
|
|
190
|
+
### Progress
|
|
191
|
+
|
|
192
|
+
A file `progress.md` is written to the `.scratch/<feature>/` directory
|
|
193
|
+
and committed after each ticket completes, so you can monitor the run from
|
|
194
|
+
another terminal by reading the file.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Keep Your Machine Awake
|
|
200
|
+
|
|
201
|
+
This is expected to run for a long time, like overnight.
|
|
202
|
+
|
|
203
|
+
### Mac OS
|
|
156
204
|
|
|
157
205
|
```sh
|
|
158
|
-
npx nightralph
|
|
206
|
+
caffeinate -i npx nightralph claude -m claude-opus-4-6
|
|
159
207
|
```
|
|
208
|
+
|
|
209
|
+
That command will prevent your machine from sleeping, but the display can still
|
|
210
|
+
go dark.
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,17 @@ import { setup, checkGhAuth } from "./setup.js";
|
|
|
10
10
|
import {
|
|
11
11
|
scanTickets,
|
|
12
12
|
dryRun,
|
|
13
|
-
|
|
13
|
+
dryRunIsolated,
|
|
14
|
+
runWaves,
|
|
15
|
+
runWavesIsolated
|
|
14
16
|
} from "./orchestrator.js";
|
|
15
17
|
import {
|
|
16
18
|
findFeatureDirs,
|
|
17
|
-
resolveIssuesDir
|
|
19
|
+
resolveIssuesDir,
|
|
20
|
+
getScriptDir
|
|
18
21
|
} from "./resolve.js";
|
|
19
|
-
|
|
22
|
+
import { getRepoRoot, getRepoName } from "./worktree.js";
|
|
23
|
+
const scriptDir = getScriptDir(import.meta.url);
|
|
20
24
|
yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").command(
|
|
21
25
|
"setup",
|
|
22
26
|
"Install bundled skills and doc templates",
|
|
@@ -71,35 +75,55 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
71
75
|
alias: "model",
|
|
72
76
|
type: "string",
|
|
73
77
|
describe: "Model passed to the agent"
|
|
74
|
-
}).option("dir", {
|
|
75
|
-
type: "string",
|
|
76
|
-
describe: "Path to the issues directory"
|
|
77
78
|
}).option("spec", {
|
|
78
79
|
type: "string",
|
|
79
|
-
describe: "
|
|
80
|
+
describe: "Feature name (resolves .scratch/<name>/)"
|
|
80
81
|
}).option("dry-run", {
|
|
81
82
|
type: "boolean",
|
|
82
83
|
describe: "Show wave order without running",
|
|
83
84
|
default: false
|
|
84
|
-
}).option("timeout
|
|
85
|
+
}).option("timeout", {
|
|
85
86
|
type: "number",
|
|
86
87
|
describe: "Kill agent after N seconds",
|
|
87
88
|
default: 300
|
|
89
|
+
}).option("X", {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
describe: "Stop on merge conflict instead of re-spawning",
|
|
92
|
+
default: false
|
|
88
93
|
}),
|
|
89
94
|
async (argv) => {
|
|
90
95
|
await runExecute({
|
|
91
96
|
provider: argv.provider,
|
|
92
97
|
model: argv.model,
|
|
93
|
-
dir: argv.dir,
|
|
94
98
|
spec: argv.spec,
|
|
95
99
|
dryRun: argv.dryRun,
|
|
96
|
-
|
|
100
|
+
timeout: argv.timeout,
|
|
101
|
+
stopOnConflict: argv.X
|
|
97
102
|
});
|
|
98
103
|
}
|
|
99
104
|
).strict().demandCommand(1, "Specify a command or provider").help().parse();
|
|
105
|
+
function isGitRepo() {
|
|
106
|
+
try {
|
|
107
|
+
getRepoRoot();
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
__name(isGitRepo, "isGitRepo");
|
|
114
|
+
function extractFeatureName(spec) {
|
|
115
|
+
const parts = spec.split(
|
|
116
|
+
/[\\/]/
|
|
117
|
+
);
|
|
118
|
+
const scratchIndex = parts.indexOf(".scratch");
|
|
119
|
+
if (scratchIndex >= 0 && scratchIndex < parts.length - 1) {
|
|
120
|
+
return parts[scratchIndex + 1];
|
|
121
|
+
}
|
|
122
|
+
return parts[parts.length - 2] ?? parts[parts.length - 1] ?? "feature";
|
|
123
|
+
}
|
|
124
|
+
__name(extractFeatureName, "extractFeatureName");
|
|
100
125
|
async function runExecute(argv) {
|
|
101
126
|
const { dir, spec } = await resolveIssuesDir(
|
|
102
|
-
argv.dir,
|
|
103
127
|
argv.spec
|
|
104
128
|
);
|
|
105
129
|
if (!existsSync(dir)) {
|
|
@@ -131,20 +155,54 @@ async function runExecute(argv) {
|
|
|
131
155
|
(b) => knownNums.has(b)
|
|
132
156
|
);
|
|
133
157
|
}
|
|
158
|
+
const logsDir = join(dirname(dir), "logs");
|
|
159
|
+
const timeout = argv.timeout ?? 300;
|
|
160
|
+
const featureName = extractFeatureName(spec);
|
|
161
|
+
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
162
|
+
const progressPath = join(
|
|
163
|
+
dirname(dir),
|
|
164
|
+
"progress.md"
|
|
165
|
+
);
|
|
134
166
|
if (argv.dryRun) {
|
|
135
|
-
|
|
167
|
+
if (isGitRepo()) {
|
|
168
|
+
const repoName = getRepoName();
|
|
169
|
+
dryRunIsolated({
|
|
170
|
+
tickets,
|
|
171
|
+
specBody,
|
|
172
|
+
repoName,
|
|
173
|
+
conflictStrategy,
|
|
174
|
+
progressPath
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
dryRun(tickets, specBody);
|
|
178
|
+
}
|
|
136
179
|
return;
|
|
137
180
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
let result;
|
|
182
|
+
if (isGitRepo()) {
|
|
183
|
+
result = await runWavesIsolated({
|
|
184
|
+
tickets,
|
|
185
|
+
specBody,
|
|
186
|
+
agentCmd: argv.provider,
|
|
187
|
+
model: argv.model ?? null,
|
|
188
|
+
timeout,
|
|
189
|
+
logsDir,
|
|
190
|
+
featureName,
|
|
191
|
+
conflictStrategy
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
result = await runWaves(
|
|
195
|
+
tickets,
|
|
196
|
+
specBody,
|
|
197
|
+
argv.provider,
|
|
198
|
+
argv.model ?? null,
|
|
199
|
+
timeout,
|
|
200
|
+
logsDir
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!result.allDone) {
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
148
206
|
}
|
|
149
207
|
__name(runExecute, "runExecute");
|
|
150
208
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { select } from '@inquirer/prompts'\nimport { setup, checkGhAuth, type TrackerKind } from './setup.js'\nimport {\n scanTickets,\n dryRun,\n runWaves,\n} from './orchestrator.js'\nimport {\n findFeatureDirs,\n resolveIssuesDir,\n} from './resolve.js'\n\nexport { findFeatureDirs, resolveIssuesDir }\n\nconst scriptDir =
|
|
5
|
-
"mappings": ";;;AAEA,SAAS,YAAY,oBAAoB;AACzC,SAAS,SAAS,YAAY;AAC9B,OAAO,WAAW;AAClB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,OAAO,mBAAqC;AACrD;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,OACG;
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { select } from '@inquirer/prompts'\nimport { setup, checkGhAuth, type TrackerKind } from './setup.js'\nimport {\n scanTickets,\n dryRun,\n dryRunIsolated,\n runWaves,\n runWavesIsolated,\n} from './orchestrator.js'\nimport {\n findFeatureDirs,\n resolveIssuesDir,\n getScriptDir,\n} from './resolve.js'\nimport { getRepoRoot, getRepoName } from './worktree.js'\n\nexport { findFeatureDirs, resolveIssuesDir }\n\nconst scriptDir = getScriptDir(import.meta.url)\n\nyargs(hideBin(process.argv))\n .scriptName('nightralph')\n .usage('$0 <command>')\n .command(\n 'setup',\n 'Install bundled skills and doc templates',\n y => y.option('claude', {\n type:'boolean',\n describe:'Also create .claude/skills/ symlinks',\n default:false,\n }),\n async argv => {\n const tracker = await select<TrackerKind>({\n message:'Issue tracker format',\n choices:[\n {\n name:'Local Markdown',\n value:'local' as const,\n description:'Issues as .md files in ' +\n '.scratch/',\n },\n {\n name:'GitHub Issues',\n value:'github' as const,\n description:'Issues via gh CLI',\n },\n ],\n })\n\n if (tracker === 'github') {\n if (!checkGhAuth()) {\n console.error(\n 'GitHub Issues requires the gh CLI, ' +\n 'authenticated.\\n' +\n 'Install: https://cli.github.com\\n' +\n 'Then run: gh auth login'\n )\n process.exit(2)\n }\n }\n\n setup({\n bundledSkillsDir:join(scriptDir, 'skills'),\n docsTemplatesDir:join(\n scriptDir, 'docs-templates'\n ),\n projectDir:process.cwd(),\n claude:argv.claude,\n tracker,\n })\n }\n )\n .command(\n '$0 <provider>',\n 'Execute tickets with the given provider',\n y => y\n .positional('provider', {\n type:'string',\n describe:'Agent CLI to spawn (e.g. claude, codex)',\n demandOption:true,\n })\n .option('m', {\n alias:'model',\n type:'string',\n describe:'Model passed to the agent',\n })\n .option('spec', {\n type:'string',\n describe:'Feature name ' +\n '(resolves .scratch/<name>/)',\n })\n .option('dry-run', {\n type:'boolean',\n describe:'Show wave order without running',\n default:false,\n })\n .option('timeout', {\n type:'number',\n describe:'Kill agent after N seconds',\n default:300,\n })\n .option('X', {\n type:'boolean',\n describe:'Stop on merge conflict instead of ' +\n 're-spawning',\n default:false,\n }),\n async argv => {\n await runExecute({\n provider:argv.provider as string,\n model:argv.model as string|undefined,\n spec:argv.spec as string|undefined,\n dryRun:argv.dryRun as boolean|undefined,\n timeout:argv.timeout as\n number|undefined,\n stopOnConflict:argv.X as\n boolean|undefined,\n })\n }\n )\n .strict()\n .demandCommand(1, 'Specify a command or provider')\n .help()\n .parse()\n\nfunction isGitRepo ():boolean {\n try {\n getRepoRoot()\n return true\n } catch {\n return false\n }\n}\n\nfunction extractFeatureName (spec:string):string {\n const parts = spec.split(\n /[\\\\/]/\n )\n const scratchIndex = parts.indexOf('.scratch')\n if (scratchIndex >= 0 && scratchIndex < parts.length - 1) {\n return parts[scratchIndex + 1]\n }\n return parts[parts.length - 2] ??\n parts[parts.length - 1] ?? 'feature'\n}\n\nasync function runExecute (argv:{\n provider:string\n model?:string\n spec?:string\n dryRun?:boolean\n timeout?:number\n stopOnConflict?:boolean\n}):Promise<void> {\n const { dir, spec } = await resolveIssuesDir(\n argv.spec\n )\n\n if (!existsSync(dir)) {\n console.error(`Directory not found: ${dir}`)\n process.exit(2)\n }\n if (!existsSync(spec)) {\n console.error(`Spec file not found: ${spec}`)\n process.exit(2)\n }\n\n const specBody = readFileSync(spec, 'utf8')\n const tickets = scanTickets(dir)\n\n if (tickets.length === 0) {\n console.log('No ticket files found. Exiting.')\n return\n }\n\n console.log(`Found ${tickets.length} tickets`)\n\n const knownNums = new Set(tickets.map(t => t.num))\n for (const t of tickets) {\n const unknown = t.blockers.filter(\n b => !knownNums.has(b)\n )\n if (unknown.length > 0) {\n console.warn(\n `${t.filename}: blockers ` +\n `[${unknown.join(', ')}] ` +\n 'not found in directory'\n )\n }\n t.blockers = t.blockers.filter(\n b => knownNums.has(b)\n )\n }\n\n const logsDir = join(dirname(dir), 'logs')\n const timeout = argv.timeout ?? 300\n const featureName = extractFeatureName(spec)\n const conflictStrategy = argv.stopOnConflict ?\n 'stop' :\n 'respawn'\n const progressPath = join(\n dirname(dir), 'progress.md'\n )\n\n if (argv.dryRun) {\n if (isGitRepo()) {\n const repoName = getRepoName()\n dryRunIsolated({\n tickets,\n specBody,\n repoName,\n conflictStrategy,\n progressPath,\n })\n } else {\n dryRun(tickets, specBody)\n }\n return\n }\n\n let result:{ allDone:boolean }\n\n if (isGitRepo()) {\n result = await runWavesIsolated({\n tickets,\n specBody,\n agentCmd:argv.provider,\n model:argv.model ?? null,\n timeout,\n logsDir,\n featureName,\n conflictStrategy,\n })\n } else {\n result = await runWaves(\n tickets, specBody,\n argv.provider, argv.model ?? null,\n timeout,\n logsDir,\n )\n }\n\n if (!result.allDone) {\n process.exit(1)\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;AAEA,SAAS,YAAY,oBAAoB;AACzC,SAAS,SAAS,YAAY;AAC9B,OAAO,WAAW;AAClB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,OAAO,mBAAqC;AACrD;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP,SAAS,aAAa,mBAAmB;AAIzC,MAAM,YAAY,aAAa,YAAY,GAAG;AAE9C,MAAM,QAAQ,QAAQ,IAAI,CAAC,EACtB,WAAW,YAAY,EACvB,MAAM,cAAc,EACpB;AAAA,EACG;AAAA,EACA;AAAA,EACA,OAAK,EAAE,OAAO,UAAU;AAAA,IACpB,MAAK;AAAA,IACL,UAAS;AAAA,IACT,SAAQ;AAAA,EACZ,CAAC;AAAA,EACD,OAAM,SAAQ;AACV,UAAM,UAAU,MAAM,OAAoB;AAAA,MACtC,SAAQ;AAAA,MACR,SAAQ;AAAA,QACJ;AAAA,UACI,MAAK;AAAA,UACL,OAAM;AAAA,UACN,aAAY;AAAA,QAEhB;AAAA,QACA;AAAA,UACI,MAAK;AAAA,UACL,OAAM;AAAA,UACN,aAAY;AAAA,QAChB;AAAA,MACJ;AAAA,IACJ,CAAC;AAED,QAAI,YAAY,UAAU;AACtB,UAAI,CAAC,YAAY,GAAG;AAChB,gBAAQ;AAAA,UACJ;AAAA,QAIJ;AACA,gBAAQ,KAAK,CAAC;AAAA,MAClB;AAAA,IACJ;AAEA,UAAM;AAAA,MACF,kBAAiB,KAAK,WAAW,QAAQ;AAAA,MACzC,kBAAiB;AAAA,QACb;AAAA,QAAW;AAAA,MACf;AAAA,MACA,YAAW,QAAQ,IAAI;AAAA,MACvB,QAAO,KAAK;AAAA,MACZ;AAAA,IACJ,CAAC;AAAA,EACL;AACJ,EACC;AAAA,EACG;AAAA,EACA;AAAA,EACA,OAAK,EACA,WAAW,YAAY;AAAA,IACpB,MAAK;AAAA,IACL,UAAS;AAAA,IACT,cAAa;AAAA,EACjB,CAAC,EACA,OAAO,KAAK;AAAA,IACT,OAAM;AAAA,IACN,MAAK;AAAA,IACL,UAAS;AAAA,EACb,CAAC,EACA,OAAO,QAAQ;AAAA,IACZ,MAAK;AAAA,IACL,UAAS;AAAA,EAEb,CAAC,EACA,OAAO,WAAW;AAAA,IACf,MAAK;AAAA,IACL,UAAS;AAAA,IACT,SAAQ;AAAA,EACZ,CAAC,EACA,OAAO,WAAW;AAAA,IACf,MAAK;AAAA,IACL,UAAS;AAAA,IACT,SAAQ;AAAA,EACZ,CAAC,EACA,OAAO,KAAK;AAAA,IACT,MAAK;AAAA,IACL,UAAS;AAAA,IAET,SAAQ;AAAA,EACZ,CAAC;AAAA,EACL,OAAM,SAAQ;AACV,UAAM,WAAW;AAAA,MACb,UAAS,KAAK;AAAA,MACd,OAAM,KAAK;AAAA,MACX,MAAK,KAAK;AAAA,MACV,QAAO,KAAK;AAAA,MACZ,SAAQ,KAAK;AAAA,MAEb,gBAAe,KAAK;AAAA,IAExB,CAAC;AAAA,EACL;AACJ,EACC,OAAO,EACP,cAAc,GAAG,+BAA+B,EAChD,KAAK,EACL,MAAM;AAEX,SAAS,YAAqB;AAC1B,MAAI;AACA,gBAAY;AACZ,WAAO;AAAA,EACX,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAPS;AAST,SAAS,mBAAoB,MAAoB;AAC7C,QAAM,QAAQ,KAAK;AAAA,IACf;AAAA,EACJ;AACA,QAAM,eAAe,MAAM,QAAQ,UAAU;AAC7C,MAAI,gBAAgB,KAAK,eAAe,MAAM,SAAS,GAAG;AACtD,WAAO,MAAM,eAAe,CAAC;AAAA,EACjC;AACA,SAAO,MAAM,MAAM,SAAS,CAAC,KACzB,MAAM,MAAM,SAAS,CAAC,KAAK;AACnC;AAVS;AAYT,eAAe,WAAY,MAOV;AACb,QAAM,EAAE,KAAK,KAAK,IAAI,MAAM;AAAA,IACxB,KAAK;AAAA,EACT;AAEA,MAAI,CAAC,WAAW,GAAG,GAAG;AAClB,YAAQ,MAAM,wBAAwB,GAAG,EAAE;AAC3C,YAAQ,KAAK,CAAC;AAAA,EAClB;AACA,MAAI,CAAC,WAAW,IAAI,GAAG;AACnB,YAAQ,MAAM,wBAAwB,IAAI,EAAE;AAC5C,YAAQ,KAAK,CAAC;AAAA,EAClB;AAEA,QAAM,WAAW,aAAa,MAAM,MAAM;AAC1C,QAAM,UAAU,YAAY,GAAG;AAE/B,MAAI,QAAQ,WAAW,GAAG;AACtB,YAAQ,IAAI,iCAAiC;AAC7C;AAAA,EACJ;AAEA,UAAQ,IAAI,SAAS,QAAQ,MAAM,UAAU;AAE7C,QAAM,YAAY,IAAI,IAAI,QAAQ,IAAI,OAAK,EAAE,GAAG,CAAC;AACjD,aAAW,KAAK,SAAS;AACrB,UAAM,UAAU,EAAE,SAAS;AAAA,MACvB,OAAK,CAAC,UAAU,IAAI,CAAC;AAAA,IACzB;AACA,QAAI,QAAQ,SAAS,GAAG;AACpB,cAAQ;AAAA,QACJ,GAAG,EAAE,QAAQ,eACT,QAAQ,KAAK,IAAI,CAAC;AAAA,MAE1B;AAAA,IACJ;AACA,MAAE,WAAW,EAAE,SAAS;AAAA,MACpB,OAAK,UAAU,IAAI,CAAC;AAAA,IACxB;AAAA,EACJ;AAEA,QAAM,UAAU,KAAK,QAAQ,GAAG,GAAG,MAAM;AACzC,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,cAAc,mBAAmB,IAAI;AAC3C,QAAM,mBAAmB,KAAK,iBAC1B,SACA;AACJ,QAAM,eAAe;AAAA,IACjB,QAAQ,GAAG;AAAA,IAAG;AAAA,EAClB;AAEA,MAAI,KAAK,QAAQ;AACb,QAAI,UAAU,GAAG;AACb,YAAM,WAAW,YAAY;AAC7B,qBAAe;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACJ,CAAC;AAAA,IACL,OAAO;AACH,aAAO,SAAS,QAAQ;AAAA,IAC5B;AACA;AAAA,EACJ;AAEA,MAAI;AAEJ,MAAI,UAAU,GAAG;AACb,aAAS,MAAM,iBAAiB;AAAA,MAC5B;AAAA,MACA;AAAA,MACA,UAAS,KAAK;AAAA,MACd,OAAM,KAAK,SAAS;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ,CAAC;AAAA,EACL,OAAO;AACH,aAAS,MAAM;AAAA,MACX;AAAA,MAAS;AAAA,MACT,KAAK;AAAA,MAAU,KAAK,SAAS;AAAA,MAC7B;AAAA,MACA;AAAA,IACJ;AAAA,EACJ;AAEA,MAAI,CAAC,OAAO,SAAS;AACjB,YAAQ,KAAK,CAAC;AAAA,EAClB;AACJ;AAnGe;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
async function mergeBranch(repoRoot, branch) {
|
|
7
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
8
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
9
|
+
}
|
|
10
|
+
if (!branch || typeof branch !== "string") {
|
|
11
|
+
throw new Error("branch must be a non-empty string");
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await execFileAsync(
|
|
15
|
+
"git",
|
|
16
|
+
["merge", branch, "--no-edit"],
|
|
17
|
+
{ cwd: repoRoot }
|
|
18
|
+
);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
try {
|
|
22
|
+
await execFileAsync(
|
|
23
|
+
"git",
|
|
24
|
+
["merge", "--abort"],
|
|
25
|
+
{ cwd: repoRoot }
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
__name(mergeBranch, "mergeBranch");
|
|
33
|
+
async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
|
|
34
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
35
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
36
|
+
}
|
|
37
|
+
if (!branch || typeof branch !== "string") {
|
|
38
|
+
throw new Error("branch must be a non-empty string");
|
|
39
|
+
}
|
|
40
|
+
if (!onto || typeof onto !== "string") {
|
|
41
|
+
throw new Error("onto must be a non-empty string");
|
|
42
|
+
}
|
|
43
|
+
if (worktreePath) {
|
|
44
|
+
try {
|
|
45
|
+
await execFileAsync(
|
|
46
|
+
"git",
|
|
47
|
+
["rebase", onto],
|
|
48
|
+
{ cwd: worktreePath }
|
|
49
|
+
);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync(
|
|
54
|
+
"git",
|
|
55
|
+
["rebase", "--abort"],
|
|
56
|
+
{ cwd: worktreePath }
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await execFileAsync(
|
|
65
|
+
"git",
|
|
66
|
+
["checkout", branch],
|
|
67
|
+
{ cwd: repoRoot }
|
|
68
|
+
);
|
|
69
|
+
await execFileAsync(
|
|
70
|
+
"git",
|
|
71
|
+
["rebase", onto],
|
|
72
|
+
{ cwd: repoRoot }
|
|
73
|
+
);
|
|
74
|
+
await execFileAsync(
|
|
75
|
+
"git",
|
|
76
|
+
["checkout", onto],
|
|
77
|
+
{ cwd: repoRoot }
|
|
78
|
+
);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
try {
|
|
82
|
+
await execFileAsync(
|
|
83
|
+
"git",
|
|
84
|
+
["rebase", "--abort"],
|
|
85
|
+
{ cwd: repoRoot }
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await execFileAsync(
|
|
91
|
+
"git",
|
|
92
|
+
["checkout", onto],
|
|
93
|
+
{ cwd: repoRoot }
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
__name(rebaseBranch, "rebaseBranch");
|
|
101
|
+
async function updateWorktreeFromBase(worktreePath, baseBranch) {
|
|
102
|
+
try {
|
|
103
|
+
await execFileAsync(
|
|
104
|
+
"git",
|
|
105
|
+
["merge", baseBranch, "--no-edit"],
|
|
106
|
+
{ cwd: worktreePath }
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
__name(updateWorktreeFromBase, "updateWorktreeFromBase");
|
|
112
|
+
async function mergeWave(opts) {
|
|
113
|
+
const { repoRoot, baseBranch, branches, strategy } = opts;
|
|
114
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
115
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
116
|
+
}
|
|
117
|
+
if (!baseBranch || typeof baseBranch !== "string") {
|
|
118
|
+
throw new Error("baseBranch must be a non-empty string");
|
|
119
|
+
}
|
|
120
|
+
const merged = [];
|
|
121
|
+
let stoppedAt = null;
|
|
122
|
+
for (let i = 0; i < branches.length; i++) {
|
|
123
|
+
const { branch, ticketNum, worktreePath } = branches[i];
|
|
124
|
+
const mergingResult = {
|
|
125
|
+
branch,
|
|
126
|
+
ticketNum,
|
|
127
|
+
merged: false,
|
|
128
|
+
conflicted: false
|
|
129
|
+
};
|
|
130
|
+
if (i === 0) {
|
|
131
|
+
const success = await mergeBranch(
|
|
132
|
+
repoRoot,
|
|
133
|
+
branch
|
|
134
|
+
);
|
|
135
|
+
mergingResult.merged = success;
|
|
136
|
+
mergingResult.conflicted = !success;
|
|
137
|
+
if (!success) {
|
|
138
|
+
if (strategy === "stop") {
|
|
139
|
+
stoppedAt = mergingResult;
|
|
140
|
+
merged.push(mergingResult);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
merged.push(mergingResult);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const rebaseSuccess = await rebaseBranch(
|
|
148
|
+
repoRoot,
|
|
149
|
+
branch,
|
|
150
|
+
baseBranch,
|
|
151
|
+
worktreePath
|
|
152
|
+
);
|
|
153
|
+
if (!rebaseSuccess) {
|
|
154
|
+
mergingResult.conflicted = true;
|
|
155
|
+
if (strategy === "stop") {
|
|
156
|
+
stoppedAt = mergingResult;
|
|
157
|
+
merged.push(mergingResult);
|
|
158
|
+
break;
|
|
159
|
+
} else {
|
|
160
|
+
merged.push(mergingResult);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const mergeSuccess = await mergeBranch(
|
|
165
|
+
repoRoot,
|
|
166
|
+
branch
|
|
167
|
+
);
|
|
168
|
+
mergingResult.merged = mergeSuccess;
|
|
169
|
+
mergingResult.conflicted = !mergeSuccess;
|
|
170
|
+
if (!mergeSuccess) {
|
|
171
|
+
if (strategy === "stop") {
|
|
172
|
+
stoppedAt = mergingResult;
|
|
173
|
+
merged.push(mergingResult);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
merged.push(mergingResult);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
merged.push(mergingResult);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
merged,
|
|
184
|
+
stoppedAt
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
__name(mergeWave, "mergeWave");
|
|
188
|
+
export {
|
|
189
|
+
mergeBranch,
|
|
190
|
+
mergeWave,
|
|
191
|
+
rebaseBranch,
|
|
192
|
+
updateWorktreeFromBase
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/merge.ts"],
|
|
4
|
+
"sourcesContent": ["import { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\n\nconst execFileAsync = promisify(execFile)\n\nexport type MergeConflictStrategy = 'respawn'|'stop'\n\nexport type MergeResult = {\n branch:string\n ticketNum:number\n merged:boolean\n conflicted:boolean\n}\n\nexport type WaveMergeResult = {\n merged:Array<MergeResult>\n stoppedAt:MergeResult|null\n}\n\nexport async function mergeBranch (\n repoRoot:string,\n branch:string\n):Promise<boolean> {\n if (!repoRoot || typeof repoRoot !== 'string') {\n throw new Error('repoRoot must be a non-empty string')\n }\n if (!branch || typeof branch !== 'string') {\n throw new Error('branch must be a non-empty string')\n }\n\n try {\n await execFileAsync(\n 'git',\n ['merge', branch, '--no-edit'],\n { cwd:repoRoot }\n )\n return true\n } catch {\n try {\n await execFileAsync(\n 'git',\n ['merge', '--abort'],\n { cwd:repoRoot }\n )\n } catch {\n // no merge in progress to abort\n }\n return false\n }\n}\n\nexport async function rebaseBranch (\n repoRoot:string,\n branch:string,\n onto:string,\n worktreePath?:string\n):Promise<boolean> {\n if (!repoRoot || typeof repoRoot !== 'string') {\n throw new Error('repoRoot must be a non-empty string')\n }\n if (!branch || typeof branch !== 'string') {\n throw new Error('branch must be a non-empty string')\n }\n if (!onto || typeof onto !== 'string') {\n throw new Error('onto must be a non-empty string')\n }\n\n // When the branch has its own worktree, it is already checked\n // out there -- checking it out in repoRoot too would fail with\n // \"already used by worktree\", so rebase in place instead.\n if (worktreePath) {\n try {\n await execFileAsync(\n 'git',\n ['rebase', onto],\n { cwd:worktreePath }\n )\n return true\n } catch {\n try {\n await execFileAsync(\n 'git',\n ['rebase', '--abort'],\n { cwd:worktreePath }\n )\n } catch {\n // ignore abort errors\n }\n return false\n }\n }\n\n try {\n await execFileAsync(\n 'git',\n ['checkout', branch],\n { cwd:repoRoot }\n )\n\n await execFileAsync(\n 'git',\n ['rebase', onto],\n { cwd:repoRoot }\n )\n\n await execFileAsync(\n 'git',\n ['checkout', onto],\n { cwd:repoRoot }\n )\n\n return true\n } catch {\n try {\n await execFileAsync(\n 'git',\n ['rebase', '--abort'],\n { cwd:repoRoot }\n )\n } catch {\n // ignore abort errors\n }\n try {\n await execFileAsync(\n 'git',\n ['checkout', onto],\n { cwd:repoRoot }\n )\n } catch {\n // ignore checkout errors\n }\n return false\n }\n}\n\n// Merges baseBranch into the worktree's branch so a\n// respawned agent sees the latest code. Conflicts are\n// left in place (not aborted) so the agent can resolve\n// and commit them.\nexport async function updateWorktreeFromBase (\n worktreePath:string,\n baseBranch:string\n):Promise<void> {\n try {\n await execFileAsync(\n 'git',\n ['merge', baseBranch, '--no-edit'],\n { cwd:worktreePath }\n )\n } catch {\n // conflict markers intentionally left for the\n // respawned agent to resolve\n }\n}\n\nexport async function mergeWave (\n opts:Readonly<{\n repoRoot:string\n baseBranch:string\n branches:ReadonlyArray<{\n branch:string\n ticketNum:number\n worktreePath?:string\n }>\n strategy:MergeConflictStrategy\n }>\n):Promise<WaveMergeResult> {\n const { repoRoot, baseBranch, branches, strategy } = opts\n\n if (!repoRoot || typeof repoRoot !== 'string') {\n throw new Error('repoRoot must be a non-empty string')\n }\n if (!baseBranch || typeof baseBranch !== 'string') {\n throw new Error('baseBranch must be a non-empty string')\n }\n\n const merged:Array<MergeResult> = []\n let stoppedAt:MergeResult|null = null\n\n for (let i = 0; i < branches.length; i++) {\n const { branch, ticketNum, worktreePath } = branches[i]\n const mergingResult:MergeResult = {\n branch,\n ticketNum,\n merged:false,\n conflicted:false,\n }\n\n if (i === 0) {\n const success = await mergeBranch(\n repoRoot,\n branch\n )\n mergingResult.merged = success\n mergingResult.conflicted = !success\n\n if (!success) {\n if (strategy === 'stop') {\n stoppedAt = mergingResult\n merged.push(mergingResult)\n break\n }\n merged.push(mergingResult)\n continue\n }\n } else {\n const rebaseSuccess = await rebaseBranch(\n repoRoot,\n branch,\n baseBranch,\n worktreePath\n )\n\n if (!rebaseSuccess) {\n mergingResult.conflicted = true\n\n if (strategy === 'stop') {\n stoppedAt = mergingResult\n merged.push(mergingResult)\n break\n } else {\n // strategy === 'respawn'\n merged.push(mergingResult)\n continue\n }\n }\n\n const mergeSuccess = await mergeBranch(\n repoRoot,\n branch\n )\n mergingResult.merged = mergeSuccess\n mergingResult.conflicted = !mergeSuccess\n\n if (!mergeSuccess) {\n if (strategy === 'stop') {\n stoppedAt = mergingResult\n merged.push(mergingResult)\n break\n }\n merged.push(mergingResult)\n continue\n }\n }\n\n merged.push(mergingResult)\n }\n\n return {\n merged,\n stoppedAt,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|