nightralph 0.0.2 → 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 +106 -33
  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).)
@@ -24,6 +26,12 @@ so they can't drift as long as you install the skills with this package.
24
26
  - [Workflow](#workflow)
25
27
  * [Create Issues](#create-issues)
26
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)
27
35
 
28
36
  <!-- tocstop -->
29
37
 
@@ -35,7 +43,11 @@ npm i -S nightralph
35
43
 
36
44
  ### Install Dependency Skills
37
45
 
38
- 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
39
51
  [Pocock skills](https://github.com/mattpocock/skills) to your repo.
40
52
  This command will copy some files in this package to a specific directory
41
53
  in your repo. The upstream
@@ -43,11 +55,6 @@ in your repo. The upstream
43
55
  skill is exposed as `/grill`, along with its sub-skills `grilling`
44
56
  and `domain-modeling`.
45
57
 
46
-
47
- ```sh
48
- npx nightralph setup
49
- ```
50
-
51
58
  The Pocock skills are bundled with this git repo, at `./skills`.
52
59
  They are cloned with some metadata, visible in
53
60
  [skill/upstream.json](skills/upstream.json).
@@ -56,24 +63,24 @@ That's how we prevent skill vs API drift.
56
63
 
57
64
  ## Workflow
58
65
 
59
- First create issues.
60
-
61
- This package bundles the skill files with the CLI, to
62
- **prevent version drift**. After installing this package,
63
- use [the `npx` command](#install-dependency-skills) to add the required
64
- skills to your local `./claude/skills/` folder or `./agents.skills/` folder.
65
-
66
- ### Create Issues
67
-
68
- Use the `to-tickets` skill to create issues in local markdown files.
69
-
70
66
  **The expected flow is**
71
67
 
72
68
  ```
73
69
  grill -> to-spec -> to-tickets -> tdd
74
70
  ```
75
71
 
76
- The `nightralph` CLI does the `tdd` part.
72
+ First create issues.
73
+
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.
77
+
78
+ ### Create Issues
79
+
80
+ Use the `to-tickets` skill to create issues in local markdown files from
81
+ a prose text description.
82
+
83
+ The `nightralph` CLI does the `tdd` part of the workflow.
77
84
 
78
85
  >
79
86
  > [!NOTE]
@@ -93,7 +100,7 @@ The `nightralph` CLI does the `tdd` part.
93
100
 
94
101
  ### Execute Tasks with `nightralph`
95
102
 
96
- After you have the tickets, you can use the `nightralph` loop to execute.
103
+ After you have the tickets, use the `nightralph` loop to execute.
97
104
  The `to-tickets` skill produces markdown files at
98
105
  `.scratch/<feature>/issues/<NN>-<slug>.md`, with task dependency info.
99
106
 
@@ -102,28 +109,32 @@ The `to-tickets` skill produces markdown files at
102
109
  npx nightralph claude -m claude-opus-4-6
103
110
 
104
111
  # Dry run to inspect the dependency graph and prompt
105
- npx nightralph claude --dry-run
112
+ npx nightralph codex --dry-run
106
113
 
107
- # Explicit paths (skips auto-discovery)
108
- npx nightralph claude -m claude-opus-4-6 \
109
- --dir .scratch/my-feature/issues \
110
- --spec .scratch/my-feature/spec.md
114
+ # Specify a feature by name
115
+ npx nightralph claude -m claude-opus-4-6 --spec my-feature
111
116
  ```
112
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
+
113
124
  Flags:
114
125
 
115
126
  | Flag | Required | Default | Description |
116
127
  |------|----------|---------|-------------|
117
128
  | `<provider>` | yes | -- | Agent CLI to spawn (e.g. `claude`, `codex`) |
118
129
  | `-m`, `--model` | no | -- | Model flag passed to the agent |
119
- | `--dir` | no | auto | Path to the issues directory |
120
- | `--spec` | no | auto | Path to the spec file |
130
+ | `--spec` | no | auto | Feature name (resolves `.scratch/<name>/`) |
121
131
  | `--dry-run` | no | -- | Show wave order and prompt without running |
122
- | `--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 |
123
134
 
124
135
  Notes:
125
136
 
126
- * When `--dir` and `--spec` are omitted, the CLI looks for
137
+ * When `--spec` is omitted, the CLI looks for
127
138
  `.scratch/<feature>/issues/` directories. If multiple features exist,
128
139
  **it prompts you to pick one** with an interactive selector.
129
140
  * Tickets are selected by `**Status:** ready-for-agent` in the file body.
@@ -131,7 +142,69 @@ Notes:
131
142
  skips tickets whose blockers haven't completed.
132
143
  * Unblocked tickets run in parallel. After a wave finishes, newly unblocked
133
144
  tickets start in the next wave.
134
- * On success (agent exit 0), the ticket's status is updated to `done`
135
- 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.
136
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`
158
+
159
+
160
+ ## How It Works
161
+
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.
165
+
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.
169
+
170
+
171
+ ### Select a ticket
172
+
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.
176
+
177
+
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
204
+
205
+ ```sh
206
+ caffeinate -i npx nightralph claude -m claude-opus-4-6
207
+ ```
137
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
+ }