nightralph 0.0.37 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,7 @@ so they can't drift as long as you install the skills with this package.
39
39
  + [`pi` + OpenRouter model](#pi--openrouter-model)
40
40
  + [`codex` + Luna](#codex--luna)
41
41
  + [`codex` + Terra](#codex--terra)
42
+ + [Mercury](#mercury)
42
43
 
43
44
  <!-- tocstop -->
44
45
 
@@ -300,7 +301,7 @@ keep the machine awake, but the display can still go dark.
300
301
  #### Claude Opus 4.6
301
302
 
302
303
  ```sh
303
- caffeinate -i npx nightralph claude -m claude-opus-4-6 --effort high
304
+ caffeinate -i npx nightralph claude -m claude-opus-4-6 --effort low
304
305
  ```
305
306
 
306
307
  #### Claude Sonnet 5
@@ -312,13 +313,9 @@ caffeinate -i npx nightralph claude -m claude-sonnet-5 --effort high
312
313
  #### `pi` + OpenRouter model
313
314
 
314
315
  ```sh
315
- caffeinate -i npx nightralph pi -m openrouter/z-ai/glm-5.3-flash --thinking high
316
+ caffeinate -i npx nightralph pi -m openrouter/z-ai/glm-5.3-flash --effort high
316
317
  ```
317
318
 
318
- ##### pi `--thinking` levels
319
-
320
- `off`, `minimal`, `low`, `medium`,`high`, and `xhigh`
321
-
322
319
  #### `codex` + Luna
323
320
 
324
321
  ```sh
@@ -328,5 +325,11 @@ caffeinate -i npx nightralph codex -m gpt-5.6-luna --effort high
328
325
  #### `codex` + Terra
329
326
 
330
327
  ```sh
331
- caffeinate -i npx nightralph codex -m gpt-5.6-luna --effort medium
328
+ caffeinate -i npx nightralph codex -m gpt-5.6-terra --effort medium
329
+ ```
330
+
331
+ #### Mercury
332
+
333
+ ```sh
334
+ caffeinate -i npx nightralph pi -m openrouter/inception/mercury-2.5 --effort high
332
335
  ```
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
4
4
 
5
5
  // src/index.ts
6
6
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
7
- import { dirname as dirname3, join as join6 } from "node:path";
7
+ import { dirname as dirname4, join as join6 } from "node:path";
8
8
  import yargs from "yargs";
9
9
  import { hideBin } from "yargs/helpers";
10
10
  import { select as select2 } from "@inquirer/prompts";
@@ -61,6 +61,17 @@ function writeManagedSection(filePath, content) {
61
61
  }
62
62
  }
63
63
  __name(writeManagedSection, "writeManagedSection");
64
+ function ensureGitAttribute(filePath, line) {
65
+ if (!existsSync(filePath)) {
66
+ writeFileSync(filePath, line + "\n");
67
+ return;
68
+ }
69
+ const existing = readFileSync(filePath, "utf8");
70
+ if (existing.split("\n").includes(line)) return;
71
+ const sep = existing.endsWith("\n") ? "" : "\n";
72
+ writeFileSync(filePath, existing + sep + line + "\n");
73
+ }
74
+ __name(ensureGitAttribute, "ensureGitAttribute");
64
75
  function checkGhAuth() {
65
76
  try {
66
77
  execSync("gh auth status", { stdio: "ignore" });
@@ -171,6 +182,11 @@ function setup(opts) {
171
182
  const agentsPath = join(projectDir, "AGENTS.md");
172
183
  writeManagedSection(agentsPath, managedContent);
173
184
  console.log("\n AGENTS.md (updated)");
185
+ ensureGitAttribute(
186
+ join(projectDir, ".gitattributes"),
187
+ "AGENTS.md merge=union"
188
+ );
189
+ console.log(" .gitattributes (AGENTS.md merge=union)");
174
190
  console.log(
175
191
  `
176
192
  Source: ${upstream.repo} @ ${upstream.commit}`
@@ -179,7 +195,7 @@ Source: ${upstream.repo} @ ${upstream.commit}`
179
195
  __name(setup, "setup");
180
196
 
181
197
  // src/orchestrator.ts
182
- import { spawn as spawn2 } from "node:child_process";
198
+ import { spawn as spawn2, execFile as execFile4 } from "node:child_process";
183
199
  import {
184
200
  createWriteStream,
185
201
  mkdirSync as mkdirSync2,
@@ -189,13 +205,14 @@ import {
189
205
  readdirSync as readdirSync2
190
206
  } from "node:fs";
191
207
  import { tmpdir } from "node:os";
192
- import { join as join4, dirname, basename as basename2 } from "node:path";
208
+ import { join as join4, dirname as dirname2, basename as basename2 } from "node:path";
193
209
  import { createInterface } from "node:readline";
210
+ import { promisify as promisify4 } from "node:util";
194
211
 
195
212
  // src/worktree.ts
196
213
  import { execFile, execFileSync } from "node:child_process";
197
214
  import { promisify } from "node:util";
198
- import { basename, join as join2 } from "node:path";
215
+ import { basename, dirname, join as join2, resolve } from "node:path";
199
216
  import { existsSync as existsSync2 } from "node:fs";
200
217
  var execFileAsync = promisify(execFile);
201
218
  function getRepoRoot() {
@@ -208,7 +225,12 @@ function getRepoRoot() {
208
225
  }
209
226
  __name(getRepoRoot, "getRepoRoot");
210
227
  function getRepoName() {
211
- return basename(getRepoRoot());
228
+ const commonDir = execFileSync(
229
+ "git",
230
+ ["rev-parse", "--git-common-dir"],
231
+ { encoding: "utf8" }
232
+ ).trim();
233
+ return basename(dirname(resolve(commonDir)));
212
234
  }
213
235
  __name(getRepoName, "getRepoName");
214
236
  function getCurrentBranch() {
@@ -220,6 +242,24 @@ function getCurrentBranch() {
220
242
  return stdout.trim();
221
243
  }
222
244
  __name(getCurrentBranch, "getCurrentBranch");
245
+ function branchExistsSync(repoRoot, branchName) {
246
+ try {
247
+ execFileSync(
248
+ "git",
249
+ [
250
+ "show-ref",
251
+ "--verify",
252
+ "--quiet",
253
+ `refs/heads/${branchName}`
254
+ ],
255
+ { cwd: repoRoot }
256
+ );
257
+ return true;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+ __name(branchExistsSync, "branchExistsSync");
223
263
  async function branchExists(repoRoot, branchName) {
224
264
  try {
225
265
  await execFileAsync(
@@ -233,11 +273,19 @@ async function branchExists(repoRoot, branchName) {
233
273
  }
234
274
  }
235
275
  __name(branchExists, "branchExists");
276
+ function buildBranchName(repoRoot, repoName, ticket) {
277
+ const num = String(ticket.num).padStart(2, "0");
278
+ const sep = branchExistsSync(repoRoot, repoName) ? "--" : "/";
279
+ return `${repoName}${sep}${num}-${ticket.slug}`;
280
+ }
281
+ __name(buildBranchName, "buildBranchName");
236
282
  async function createWorktreeNow(opts) {
237
283
  const { repoRoot, repoName, baseBranch, ticket } = opts;
238
- const branchName = `${repoName}/${String(
239
- ticket.num
240
- ).padStart(2, "0")}-${ticket.slug}`;
284
+ const branchName = buildBranchName(
285
+ repoRoot,
286
+ repoName,
287
+ ticket
288
+ );
241
289
  const worktreePath = join2(
242
290
  repoRoot,
243
291
  "..",
@@ -1026,7 +1074,7 @@ function detectTestCmd(projectDir) {
1026
1074
  }
1027
1075
  __name(detectTestCmd, "detectTestCmd");
1028
1076
  function runTestCmd(opts) {
1029
- return new Promise((resolve) => {
1077
+ return new Promise((resolve2) => {
1030
1078
  let settled = false;
1031
1079
  const child = spawn("sh", ["-c", opts.cmd], {
1032
1080
  cwd: opts.cwd,
@@ -1058,7 +1106,7 @@ function runTestCmd(opts) {
1058
1106
  opts.registry.delete(child.pid);
1059
1107
  }
1060
1108
  chunks.push(String(err));
1061
- resolve({
1109
+ resolve2({
1062
1110
  exitCode: 1,
1063
1111
  output: chunks.join("")
1064
1112
  });
@@ -1070,7 +1118,7 @@ function runTestCmd(opts) {
1070
1118
  if (child.pid !== void 0 && opts.registry) {
1071
1119
  opts.registry.delete(child.pid);
1072
1120
  }
1073
- resolve({
1121
+ resolve2({
1074
1122
  exitCode: code ?? 1,
1075
1123
  output: chunks.join("")
1076
1124
  });
@@ -1080,6 +1128,7 @@ function runTestCmd(opts) {
1080
1128
  __name(runTestCmd, "runTestCmd");
1081
1129
 
1082
1130
  // src/orchestrator.ts
1131
+ var execFileAsync4 = promisify4(execFile4);
1083
1132
  function formatErrorMessage(error) {
1084
1133
  return error instanceof Error ? error.message : String(error);
1085
1134
  }
@@ -1094,6 +1143,48 @@ async function removeWorktreeQuietly(worktreePath, d) {
1094
1143
  }
1095
1144
  }
1096
1145
  __name(removeWorktreeQuietly, "removeWorktreeQuietly");
1146
+ async function commitTicketFiles(repoRoot, tickets, d) {
1147
+ const paths = tickets.map((t) => t.filepath);
1148
+ if (paths.length === 0) return;
1149
+ try {
1150
+ await execFileAsync4(
1151
+ "git",
1152
+ ["add", "--force", ...paths],
1153
+ { cwd: repoRoot }
1154
+ );
1155
+ try {
1156
+ await execFileAsync4(
1157
+ "git",
1158
+ [
1159
+ "diff",
1160
+ "--cached",
1161
+ "--quiet",
1162
+ "--",
1163
+ ...paths
1164
+ ],
1165
+ { cwd: repoRoot }
1166
+ );
1167
+ return;
1168
+ } catch {
1169
+ }
1170
+ await execFileAsync4(
1171
+ "git",
1172
+ [
1173
+ "commit",
1174
+ "-m",
1175
+ "nightralph: update ticket checklists",
1176
+ "--",
1177
+ ...paths
1178
+ ],
1179
+ { cwd: repoRoot }
1180
+ );
1181
+ } catch (err) {
1182
+ d.log(
1183
+ " Failed to commit ticket updates: " + formatErrorMessage(err)
1184
+ );
1185
+ }
1186
+ }
1187
+ __name(commitTicketFiles, "commitTicketFiles");
1097
1188
  var TICKET_RE = /^(\d+)-(.+)\.md$/;
1098
1189
  function parseTicketFilename(filename) {
1099
1190
  const m = filename.match(TICKET_RE);
@@ -1305,14 +1396,18 @@ function renderPrompt(specBody, ticket, opts) {
1305
1396
  "You are an autonomous coding agent. Implement the ticket using /tdd.",
1306
1397
  "",
1307
1398
  "Follow these rules:",
1308
- "- Read the files directly relevant to the ticket before writing code.",
1399
+ "- Read the files directly relevant to the ticket before writing code. Also read any AGENTS.md in or above the directories you will touch, plus CONTEXT.md and the relevant docs/adr/ entries if they exist. If they do not exist, proceed silently.",
1400
+ "- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
1309
1401
  "- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
1310
1402
  "- Use /tdd for every checklist item. For each item, write a failing test at the seams named in the spec's Testing Decisions section, then write only enough code to make it pass, then refactor.",
1311
- `- Run \`${opts.testCmd}\` before committing. Do not commit with failing tests. The orchestrator runs the same command after you exit and rejects the ticket if it fails.`,
1403
+ `- While iterating, run only the tests relevant to what you changed. Run \`${opts.testCmd}\` in full once, as the final gate before your last commit. Do not commit with failing tests. The orchestrator runs the same command after you exit and rejects the ticket if it fails.`,
1404
+ "- If the project has a lint script, run it and fix what it reports.",
1405
+ "- INTEGRITY: never delete, skip, weaken, or write tautological tests to force a green result. Only report a checklist item done when its real tests genuinely pass. If an item cannot be completed honestly, leave it unchecked and say what blocked you.",
1312
1406
  "- Do NOT read or modify files outside your current directory (the worktree).",
1313
1407
  "- Do NOT run git push.",
1314
- "- Commit your work when finished.",
1408
+ "- Commit after each checklist item, and make sure everything is committed when finished.",
1315
1409
  "- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
1410
+ "- Before your final commit, record reusable learnings in the nearest AGENTS.md (in or above the directories you modified; prefer that over the repo root). Good additions: module conventions, gotchas, files that must change together, how to test that area, environment requirements. Do NOT add ticket-specific details or temporary debugging notes. Only write when you have knowledge that would help future work in that directory.",
1316
1411
  ""
1317
1412
  ].join("\n");
1318
1413
  }
@@ -1333,8 +1428,9 @@ function renderMergePrompt(specBody, ticket) {
1333
1428
  "",
1334
1429
  "Your task:",
1335
1430
  "1. Find and resolve all conflict markers (<<<<<<< / ======= / >>>>>>>).",
1336
- "2. Make sure the code compiles and tests pass.",
1337
- "3. Commit the resolved files.",
1431
+ "2. When AGENTS.md or other docs conflict, keep both sides' additions rather than picking one.",
1432
+ "3. Make sure the code compiles and tests pass.",
1433
+ "4. Commit the resolved files.",
1338
1434
  "",
1339
1435
  "Do NOT re-implement the ticket from scratch.",
1340
1436
  "Do NOT read or modify files outside your current directory (the worktree).",
@@ -1748,7 +1844,7 @@ function spawnAgent(opts) {
1748
1844
  proc.stdin.write(opts.prompt);
1749
1845
  }
1750
1846
  proc.stdin.end();
1751
- return new Promise((resolve) => {
1847
+ return new Promise((resolve2) => {
1752
1848
  let settled = false;
1753
1849
  let graceTimer;
1754
1850
  function finish(exitCode) {
@@ -1774,7 +1870,7 @@ function spawnAgent(opts) {
1774
1870
  rlErr.close();
1775
1871
  logStream.end();
1776
1872
  if (promptFile) cleanupPromptFile(promptFile);
1777
- resolve({ exitCode, completedItems, timedOut });
1873
+ resolve2({ exitCode, completedItems, timedOut });
1778
1874
  }
1779
1875
  __name(finish, "finish");
1780
1876
  proc.on("exit", (code) => {
@@ -1804,7 +1900,7 @@ function spawnAgent(opts) {
1804
1900
  "Agent process error:",
1805
1901
  formatErrorMessage(err)
1806
1902
  );
1807
- resolve({
1903
+ resolve2({
1808
1904
  exitCode: 1,
1809
1905
  completedItems,
1810
1906
  timedOut
@@ -1855,7 +1951,11 @@ function dryRunIsolated(opts) {
1855
1951
  );
1856
1952
  for (const t of ready) {
1857
1953
  const num = String(t.num).padStart(2, "0");
1858
- const branchName = `${opts.repoName}/${num}-${t.slug}`;
1954
+ const branchName = buildBranchName(
1955
+ opts.repoRoot,
1956
+ opts.repoName,
1957
+ t
1958
+ );
1859
1959
  const worktreePath = `../${opts.repoName}.${num}-${t.slug}`;
1860
1960
  console.log(` worktree: ${worktreePath}`);
1861
1961
  console.log(` branch: ${branchName}`);
@@ -2022,7 +2122,7 @@ function retryDelayFor(base, attempt) {
2022
2122
  }
2023
2123
  __name(retryDelayFor, "retryDelayFor");
2024
2124
  function sleep(ms) {
2025
- return new Promise((resolve) => setTimeout(resolve, ms));
2125
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2026
2126
  }
2027
2127
  __name(sleep, "sleep");
2028
2128
  function attemptLogPath(logsDir, ticket, attempt, ext) {
@@ -2355,7 +2455,7 @@ async function runWavesIsolated(opts) {
2355
2455
  );
2356
2456
  }
2357
2457
  const progressPath = join4(
2358
- dirname(opts.logsDir),
2458
+ dirname2(opts.logsDir),
2359
2459
  "progress.md"
2360
2460
  );
2361
2461
  const progress = mergeProgress(
@@ -2517,6 +2617,13 @@ async function runWavesIsolated(opts) {
2517
2617
  });
2518
2618
  }
2519
2619
  if (successfulBranches.length > 0) {
2620
+ await commitTicketFiles(
2621
+ repoRoot,
2622
+ successfulBranches.map(
2623
+ (sb) => ticketsByNum.get(sb.ticketNum)
2624
+ ),
2625
+ d
2626
+ );
2520
2627
  const mergeResult = await mergeWave({
2521
2628
  repoRoot,
2522
2629
  baseBranch,
@@ -2683,11 +2790,11 @@ __name(runWavesIsolated, "runWavesIsolated");
2683
2790
 
2684
2791
  // src/resolve.ts
2685
2792
  import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
2686
- import { dirname as dirname2, join as join5 } from "node:path";
2793
+ import { dirname as dirname3, join as join5 } from "node:path";
2687
2794
  import { fileURLToPath } from "node:url";
2688
2795
  import { select } from "@inquirer/prompts";
2689
2796
  function getScriptDir(metaUrl) {
2690
- return dirname2(fileURLToPath(metaUrl));
2797
+ return dirname3(fileURLToPath(metaUrl));
2691
2798
  }
2692
2799
  __name(getScriptDir, "getScriptDir");
2693
2800
  function findFeatureDirs(cwd) {
@@ -2914,7 +3021,7 @@ async function runExecute(argv) {
2914
3021
  (b) => knownNums.has(b)
2915
3022
  );
2916
3023
  }
2917
- const logsDir = join6(dirname3(dir), "logs");
3024
+ const logsDir = join6(dirname4(dir), "logs");
2918
3025
  const timeout = argv.timeout ?? 900;
2919
3026
  const testCmd = argv.testCmd || detectTestCmd(
2920
3027
  isGitRepo() ? getRepoRoot() : process.cwd()
@@ -2922,15 +3029,17 @@ async function runExecute(argv) {
2922
3029
  const featureName = extractFeatureName(spec);
2923
3030
  const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
2924
3031
  const progressPath = join6(
2925
- dirname3(dir),
3032
+ dirname4(dir),
2926
3033
  "progress.md"
2927
3034
  );
2928
3035
  if (argv.dryRun) {
2929
3036
  if (isGitRepo()) {
3037
+ const repoRoot = getRepoRoot();
2930
3038
  const repoName = getRepoName();
2931
3039
  dryRunIsolated({
2932
3040
  tickets,
2933
3041
  specBody,
3042
+ repoRoot,
2934
3043
  repoName,
2935
3044
  conflictStrategy,
2936
3045
  progressPath,