nightralph 0.0.34 → 0.0.36

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
@@ -318,4 +318,4 @@ caffeinate -i npx nightralph pi -m openrouter/z-ai/glm-5.3-flash
318
318
 
319
319
  ```sh
320
320
  caffeinate -i npx nightralph codex -m gpt-5.6-luna
321
- ```
321
+ ```
@@ -6,18 +6,26 @@ Issues and specs for this repo live as markdown files in `.scratch/`.
6
6
 
7
7
  - One feature per directory: `.scratch/<feature-slug>/`
8
8
  - The spec is `.scratch/<feature-slug>/spec.md`
9
- - Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`, never a single combined tickets file
10
- - Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
11
- - Dependencies are recorded as a `**Blocked by:**` line near the top of each issue file, listing the numbers of the tickets that must complete first, or `None`
12
- - Comments and conversation history append to the bottom of the file under a `## Comments` heading
9
+ - Implementation issues are one file per ticket at
10
+ `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`,
11
+ never a single combined tickets file
12
+ - Triage state is recorded as a `**Status:**` line near the top of each
13
+ issue file (see `triage-labels.md` for the role strings)
14
+ - Dependencies are recorded as a `**Blocked by:**` line near the top of
15
+ each issue file, listing the numbers of the tickets that must complete
16
+ first, or `None`
17
+ - Comments and conversation history append to the bottom of the file under
18
+ a `## Comments` heading
13
19
 
14
20
  ## When a skill says "publish to the issue tracker"
15
21
 
16
- Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
22
+ Create a new file under `.scratch/<feature-slug>/` (creating the directory
23
+ if needed).
17
24
 
18
25
  ## When a skill says "fetch the relevant ticket"
19
26
 
20
- Read the file at the referenced path. The user will normally pass the path or the issue number directly.
27
+ Read the file at the referenced path. The user will normally pass the path
28
+ or the issue number directly.
21
29
 
22
30
  ## Tickets are executed within hours, not weeks
23
31
 
package/dist/index.js CHANGED
@@ -1292,11 +1292,7 @@ function stripSpecSection(specBody, heading) {
1292
1292
  return out.join("\n");
1293
1293
  }
1294
1294
  __name(stripSpecSection, "stripSpecSection");
1295
- function renderPrompt(specBody, ticket, opts = {}) {
1296
- const testRules = opts.testCmd ? [
1297
- "- Work test-first. For each checklist 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.",
1298
- `- 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.`
1299
- ] : [];
1295
+ function renderPrompt(specBody, ticket, opts) {
1300
1296
  return [
1301
1297
  "---- SPEC CONTEXT ----",
1302
1298
  specBody,
@@ -1306,12 +1302,13 @@ function renderPrompt(specBody, ticket, opts = {}) {
1306
1302
  ticket.body,
1307
1303
  "",
1308
1304
  "---- AGENT INSTRUCTIONS ----",
1309
- "You are an autonomous coding agent. Implement the ticket.",
1305
+ "You are an autonomous coding agent. Implement the ticket using /tdd.",
1310
1306
  "",
1311
1307
  "Follow these rules:",
1312
1308
  "- Read the files directly relevant to the ticket before writing code.",
1313
1309
  "- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
1314
- ...testRules,
1310
+ "- 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.`,
1315
1312
  "- Do NOT read or modify files outside your current directory (the worktree).",
1316
1313
  "- Do NOT run git push.",
1317
1314
  "- Commit your work when finished.",
@@ -1816,7 +1813,7 @@ function spawnAgent(opts) {
1816
1813
  });
1817
1814
  }
1818
1815
  __name(spawnAgent, "spawnAgent");
1819
- function dryRun(tickets, specBody) {
1816
+ function dryRun(tickets, specBody, testCmd) {
1820
1817
  listTickets(tickets);
1821
1818
  console.log("\nSimulated wave order:");
1822
1819
  for (const { wave, ready } of simulateWaves(tickets)) {
@@ -1838,7 +1835,11 @@ Prompt for first ready ticket (${firstReady[0].filename}):
1838
1835
  `
1839
1836
  );
1840
1837
  console.log(
1841
- renderPrompt(specBody, firstReady[0])
1838
+ renderPrompt(
1839
+ specBody,
1840
+ firstReady[0],
1841
+ { testCmd }
1842
+ )
1842
1843
  );
1843
1844
  }
1844
1845
  }
@@ -1884,7 +1885,7 @@ Prompt for first ready ticket (${firstReady[0].filename}):
1884
1885
  const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
1885
1886
  console.log(
1886
1887
  `
1887
- Test command: ${opts.testCmd ?? "(none)"}`
1888
+ Test command: ${opts.testCmd}`
1888
1889
  );
1889
1890
  console.log(
1890
1891
  `Effort: ${opts.effort ?? "(provider default)"}`
@@ -1976,22 +1977,20 @@ async function respawnAndRetryMerge(opts) {
1976
1977
  }
1977
1978
  return false;
1978
1979
  }
1979
- if (opts.testCmd) {
1980
- const passed = await runTestGate({
1981
- testCmd: opts.testCmd,
1982
- ticket: opts.ticket,
1983
- worktreePath: opts.worktreePath,
1984
- timeout: opts.timeout,
1985
- logsDir: opts.logsDir,
1986
- log,
1987
- registry: runningAgents
1988
- });
1989
- if (!passed) {
1990
- log(
1991
- ` ${opts.ticket.filename}: tests failed after conflict respawn; not merging`
1992
- );
1993
- return false;
1994
- }
1980
+ const passed = await runTestGate({
1981
+ testCmd: opts.testCmd,
1982
+ ticket: opts.ticket,
1983
+ worktreePath: opts.worktreePath,
1984
+ timeout: opts.timeout,
1985
+ logsDir: opts.logsDir,
1986
+ log,
1987
+ registry: runningAgents
1988
+ });
1989
+ if (!passed) {
1990
+ log(
1991
+ ` ${opts.ticket.filename}: tests failed after conflict respawn; not merging`
1992
+ );
1993
+ return false;
1995
1994
  }
1996
1995
  const mergeResult = await mergeBranch(
1997
1996
  opts.repoRoot,
@@ -2107,30 +2106,28 @@ async function runTicketAttempt(opts) {
2107
2106
  branchName: worktree.branchName
2108
2107
  };
2109
2108
  }
2110
- if (opts.testCmd) {
2111
- const passed = await runTestGate({
2112
- testCmd: opts.testCmd,
2113
- ticket,
2114
- worktreePath: worktree.worktreePath,
2115
- timeout: opts.timeout,
2116
- logsDir: opts.logsDir,
2117
- attempt: opts.attempt,
2118
- log: /* @__PURE__ */ __name((msg) => d.log(msg), "log"),
2119
- registry: runningAgents
2120
- });
2121
- if (!passed) {
2122
- await removeWorktreeQuietly(
2123
- worktree.worktreePath,
2124
- d
2125
- );
2126
- return {
2127
- kind: "failed",
2128
- reason: "tests",
2129
- branchName: worktree.branchName,
2130
- exitCode: 1,
2131
- completedItems: agentResult.completedItems
2132
- };
2133
- }
2109
+ const passed = await runTestGate({
2110
+ testCmd: opts.testCmd,
2111
+ ticket,
2112
+ worktreePath: worktree.worktreePath,
2113
+ timeout: opts.timeout,
2114
+ logsDir: opts.logsDir,
2115
+ attempt: opts.attempt,
2116
+ log: /* @__PURE__ */ __name((msg) => d.log(msg), "log"),
2117
+ registry: runningAgents
2118
+ });
2119
+ if (!passed) {
2120
+ await removeWorktreeQuietly(
2121
+ worktree.worktreePath,
2122
+ d
2123
+ );
2124
+ return {
2125
+ kind: "failed",
2126
+ reason: "tests",
2127
+ branchName: worktree.branchName,
2128
+ exitCode: 1,
2129
+ completedItems: agentResult.completedItems
2130
+ };
2134
2131
  }
2135
2132
  return {
2136
2133
  kind: "ready",
@@ -2204,7 +2201,7 @@ function runProgress(tickets) {
2204
2201
  };
2205
2202
  }
2206
2203
  __name(runProgress, "runProgress");
2207
- async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, display, maxTurns, effort, retries, retryDelay) {
2204
+ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay) {
2208
2205
  mkdirSync2(logsDir, { recursive: true });
2209
2206
  const d = display ?? createDisplay();
2210
2207
  const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
@@ -2241,7 +2238,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
2241
2238
  );
2242
2239
  const results = await Promise.allSettled(
2243
2240
  ready.map(async (t) => {
2244
- const prompt = renderPrompt(specBody, t);
2241
+ const prompt = renderPrompt(
2242
+ specBody,
2243
+ t,
2244
+ { testCmd }
2245
+ );
2245
2246
  const label = t.filename.replace(
2246
2247
  /\.md$/,
2247
2248
  ""
@@ -2368,9 +2369,6 @@ async function runWavesIsolated(opts) {
2368
2369
  `nightralph: start ${opts.featureName}`,
2369
2370
  d
2370
2371
  );
2371
- if (!opts.testCmd) {
2372
- d.log(" Test gate disabled: no test command");
2373
- }
2374
2372
  let totalCompleted = 0;
2375
2373
  let waveNum = 0;
2376
2374
  const failedNums = /* @__PURE__ */ new Set();
@@ -2823,7 +2821,7 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
2823
2821
  default: 3600
2824
2822
  }).option("test-cmd", {
2825
2823
  type: "string",
2826
- describe: 'Command run in each worktree after the agent exits; non-zero exit rejects the ticket. Defaults to "npm test" when package.json has a test script. Pass "" to disable.'
2824
+ describe: 'Command run in each worktree after the agent exits; non-zero exit rejects the ticket. Defaults to "npm test".'
2827
2825
  }).option("X", {
2828
2826
  type: "boolean",
2829
2827
  describe: "Stop on merge conflict instead of re-spawning",
@@ -2918,9 +2916,9 @@ async function runExecute(argv) {
2918
2916
  }
2919
2917
  const logsDir = join6(dirname3(dir), "logs");
2920
2918
  const timeout = argv.timeout ?? 900;
2921
- const testCmd = argv.testCmd === void 0 ? detectTestCmd(
2919
+ const testCmd = argv.testCmd || detectTestCmd(
2922
2920
  isGitRepo() ? getRepoRoot() : process.cwd()
2923
- ) : argv.testCmd || null;
2921
+ ) || "npm test";
2924
2922
  const featureName = extractFeatureName(spec);
2925
2923
  const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
2926
2924
  const progressPath = join6(
@@ -2942,7 +2940,7 @@ async function runExecute(argv) {
2942
2940
  retryDelay: argv.retryDelay
2943
2941
  });
2944
2942
  } else {
2945
- dryRun(tickets, specBody);
2943
+ dryRun(tickets, specBody, testCmd);
2946
2944
  }
2947
2945
  return;
2948
2946
  }
@@ -2981,6 +2979,7 @@ async function runExecute(argv) {
2981
2979
  argv.model ?? null,
2982
2980
  timeout,
2983
2981
  logsDir,
2982
+ testCmd,
2984
2983
  display,
2985
2984
  argv.maxTurns,
2986
2985
  argv.effort,