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.
Files changed (93) hide show
  1. package/README.md +103 -52
  2. package/dist/index.js +80 -22
  3. package/dist/index.js.map +2 -2
  4. package/dist/merge.js +194 -0
  5. package/dist/merge.js.map +7 -0
  6. package/dist/meta.json +183 -17
  7. package/dist/orchestrator.js +459 -44
  8. package/dist/orchestrator.js.map +2 -2
  9. package/dist/progress.js +164 -0
  10. package/dist/progress.js.map +7 -0
  11. package/dist/resolve.js +16 -8
  12. package/dist/resolve.js.map +2 -2
  13. package/dist/setup.js +33 -5
  14. package/dist/setup.js.map +2 -2
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/merge.d.ts +25 -0
  17. package/dist/src/merge.d.ts.map +1 -0
  18. package/dist/src/orchestrator.d.ts +27 -3
  19. package/dist/src/orchestrator.d.ts.map +1 -1
  20. package/dist/src/progress.d.ts +27 -0
  21. package/dist/src/progress.d.ts.map +1 -0
  22. package/dist/src/resolve.d.ts +2 -1
  23. package/dist/src/resolve.d.ts.map +1 -1
  24. package/dist/src/setup.d.ts +1 -0
  25. package/dist/src/setup.d.ts.map +1 -1
  26. package/dist/src/worktree.d.ts +18 -0
  27. package/dist/src/worktree.d.ts.map +1 -0
  28. package/dist/worktree.js +139 -0
  29. package/dist/worktree.js.map +7 -0
  30. package/package.json +8 -4
  31. package/.claude/settings.local.json +0 -5
  32. package/.github/dependabot.yml +0 -28
  33. package/.github/workflows/nodejs.yml +0 -30
  34. package/AGENTS.md +0 -60
  35. package/CLAUDE.md +0 -1
  36. package/PROMPT.md +0 -95
  37. package/WIP.md +0 -5
  38. package/dist/test/integration.test.js +0 -848
  39. package/dist/test/integration.test.js.map +0 -7
  40. package/dist/test/orchestrator.test.js +0 -749
  41. package/dist/test/orchestrator.test.js.map +0 -7
  42. package/dist/test/setup.test.js +0 -574
  43. package/dist/test/setup.test.js.map +0 -7
  44. package/docs/README.md +0 -12
  45. package/docs/agents/domain.md +0 -36
  46. package/docs/agents/issue-tracker.md +0 -30
  47. package/docs/agents/triage-labels.md +0 -15
  48. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +0 -144
  49. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +0 -292
  50. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +0 -212
  51. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +0 -203
  52. package/docs/skills.md +0 -58
  53. package/docs/test-plans/2026-08-27-issue-tracker-choice.md +0 -95
  54. package/eslint.config.js +0 -142
  55. package/lib.es5.d.ts +0 -19
  56. package/progress.log +0 -190
  57. package/ralph_claude.sh +0 -263
  58. package/scripts/clone-skills.sh +0 -52
  59. package/specs/prd.json +0 -172
  60. package/src/docs-templates/domain.md +0 -36
  61. package/src/docs-templates/issue-tracker-github.md +0 -228
  62. package/src/docs-templates/issue-tracker.md +0 -30
  63. package/src/docs-templates/triage-labels.md +0 -15
  64. package/src/index.ts +0 -184
  65. package/src/orchestrator.ts +0 -324
  66. package/src/resolve.ts +0 -73
  67. package/src/setup.ts +0 -157
  68. package/src/skills/domain-modeling/ADR-FORMAT.md +0 -47
  69. package/src/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  70. package/src/skills/domain-modeling/SKILL.md +0 -74
  71. package/src/skills/domain-modeling/agents/openai.yaml +0 -3
  72. package/src/skills/grill/SKILL.md +0 -7
  73. package/src/skills/grill/agents/openai.yaml +0 -5
  74. package/src/skills/grilling/SKILL.md +0 -28
  75. package/src/skills/grilling/agents/openai.yaml +0 -3
  76. package/src/skills/tdd/SKILL.md +0 -38
  77. package/src/skills/tdd/agents/openai.yaml +0 -3
  78. package/src/skills/tdd/mocking.md +0 -59
  79. package/src/skills/tdd/tests.md +0 -77
  80. package/src/skills/to-spec/SKILL.md +0 -75
  81. package/src/skills/to-spec/agents/openai.yaml +0 -5
  82. package/src/skills/to-tickets/SKILL.md +0 -105
  83. package/src/skills/to-tickets/agents/openai.yaml +0 -5
  84. package/src/skills/upstream.json +0 -5
  85. package/tasks/prd-integration-test-coverage.md +0 -243
  86. package/test/integration.test.ts +0 -636
  87. package/test/mock-agent-echo-args.js +0 -4
  88. package/test/mock-agent-fail.js +0 -3
  89. package/test/mock-agent-pass.js +0 -3
  90. package/test/orchestrator.test.ts +0 -609
  91. package/test/setup.test.ts +0 -417
  92. package/tsconfig.build.json +0 -7
  93. package/tsconfig.json +0 -31
package/README.md CHANGED
@@ -6,13 +6,15 @@
6
6
  [![license](https://img.shields.io/badge/license-Big_Time-blue?style=flat-square)](LICENSE)
7
7
 
8
8
  This is a minimal Node-based AI orchestrator. It reads local markdown tickets
9
- or remote Github issues produced by
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 that the API in `nightshift` had already drifted
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 Issues](#execute-issues)
27
- - [Workflow](#workflow-1)
28
- * [`nightralph` command](#nightralph-command)
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
- Use a subcommand in this package to add the required
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
- This package bundles the skill files with the CLI, to
64
- **prevent version drift**. After installing this package,
65
- use [the `npx` command](#install-dependency-skills) to add the required
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
- **The expected flow is**
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
- `to-tickets` produces markdown files at
93
- `.scratch/<feature>/issues/<NN>-<slug>.md`.
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 claude --dry-run
112
+ npx nightralph codex --dry-run
101
113
 
102
- # Explicit paths (skips auto-discovery)
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
- | `--dir` | no | auto | Path to the issues directory |
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
- | `--timeout-secs` | no | `300` | Kill agent after N seconds |
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 `--dir` and `--spec` are omitted, the CLI looks for
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
- * On success (agent exit 0), the ticket's status is updated to `done`
130
- in-place. Failed tickets keep their status and block dependents.
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
- ## Workflow
160
+ ## How It Works
135
161
 
136
- Need to use several skills, in order:
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
- grill -> to-spec -> to-tickets
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
- ### `nightralph` command
171
+ ### Select a ticket
148
172
 
149
- After you have the tickets, you can use the `nightralph` loop to execute.
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
- Or with a different provider:
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 codex -m gpt-5.6-terra
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
- runWaves
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
- const scriptDir = dirname(process.argv[1]);
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: "Path to the spec file"
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-secs", {
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
- timeoutSecs: argv.timeoutSecs
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
- dryRun(tickets, specBody);
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
- const logsDir = join(dirname(dir), "logs");
139
- const timeoutSecs = argv.timeoutSecs ?? 300;
140
- await runWaves(
141
- tickets,
142
- specBody,
143
- argv.provider,
144
- argv.model ?? null,
145
- timeoutSecs,
146
- logsDir
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 = dirname(process.argv[1])\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('dir', {\n type:'string',\n describe:'Path to the issues directory',\n })\n .option('spec', {\n type:'string',\n describe:'Path to the spec file',\n })\n .option('dry-run', {\n type:'boolean',\n describe:'Show wave order without running',\n default:false,\n })\n .option('timeout-secs', {\n type:'number',\n describe:'Kill agent after N seconds',\n default:300,\n }),\n async argv => {\n await runExecute({\n provider:argv.provider as string,\n model:argv.model as string|undefined,\n dir:argv.dir as string|undefined,\n spec:argv.spec as string|undefined,\n dryRun:argv.dryRun as boolean|undefined,\n timeoutSecs:argv.timeoutSecs as\n number|undefined,\n })\n }\n )\n .strict()\n .demandCommand(1, 'Specify a command or provider')\n .help()\n .parse()\n\nasync function runExecute (argv:{\n provider:string\n model?:string\n dir?:string\n spec?:string\n dryRun?:boolean\n timeoutSecs?:number\n}):Promise<void> {\n const { dir, spec } = await resolveIssuesDir(\n argv.dir, 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 if (argv.dryRun) {\n dryRun(tickets, specBody)\n return\n }\n\n const logsDir = join(dirname(dir), 'logs')\n const timeoutSecs = argv.timeoutSecs ?? 300\n await runWaves(\n tickets, specBody,\n argv.provider, argv.model ?? null, timeoutSecs,\n logsDir,\n )\n}\n"],
5
- "mappings": "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",
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": "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",
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
+ }