@levistudio/redline 0.5.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to Redline are documented here. The format follows [Keep a C
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.1] - 2026-05-15
8
+
9
+ ### Fixed
10
+ - `redline-review` now launches Redline with `nohup` so the local server survives short-lived agent shell calls instead of ending the browser session early.
11
+ - `redline author-wait` now returns `kind: "session-ended"` when the startup PID is gone and no result was written, so agents do not wait forever on a dead session.
12
+
7
13
  ## [0.5.0] - 2026-05-15
8
14
 
9
15
  ### Added
@@ -73,7 +79,8 @@ Initial public release on npm as `@levistudio/redline`.
73
79
  - Auto-installs missing dependencies on first CLI run.
74
80
  - Initial test suite: server, sidecar, parsing, model-picking, rendering, diff, SSE, integration, happy-dom client.
75
81
 
76
- [Unreleased]: https://github.com/alevi/redline/compare/v0.5.0...HEAD
82
+ [Unreleased]: https://github.com/alevi/redline/compare/v0.5.1...HEAD
83
+ [0.5.1]: https://github.com/alevi/redline/compare/v0.5.0...v0.5.1
77
84
  [0.5.0]: https://github.com/alevi/redline/compare/v0.4.1...v0.5.0
78
85
  [0.4.1]: https://github.com/alevi/redline/compare/v0.4.0...v0.4.1
79
86
  [0.4.0]: https://github.com/alevi/redline/compare/v0.3.0...v0.4.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levistudio/redline",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Inline comments on Markdown files, for human-in-the-loop AI doc review.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -11,7 +11,7 @@ When you've produced a markdown document that the human needs to read, comment o
11
11
 
12
12
  The redline launcher lives at `__REDLINE_BIN__` (substituted at install time — if you see the literal placeholder string, the skill was installed incorrectly; tell the human to re-run `redline install-skill`). Always invoke it by this absolute path. Do not call bare `redline` and do not try to "fix" PATH issues by running `bun link` or guessing where the repo lives.
13
13
 
14
- **Always background the launcher and poll.** Never run `__REDLINE_BIN__` as a foreground/blocking shell call: agent shell tools often buffer stdout until the process exits, so you would never see the URL the human needs to click and your "I'll wait while you review" message would be a lie. Use this pattern:
14
+ **Always detach the launcher with `nohup` and poll.** Never run `__REDLINE_BIN__` as a foreground/blocking shell call: agent shell tools often buffer stdout until the process exits, so you would never see the URL the human needs to click and your "I'll wait while you review" message would be a lie. Also do not use a plain trailing `&` without `nohup`: in Codex-style short shell calls, the shell can exit and take the Redline server with it. Use this pattern:
15
15
 
16
16
  ```bash
17
17
  FILE=/abs/path/to/file.md
@@ -20,8 +20,8 @@ STARTUP="$DIR/.review/$BASE.startup.json"
20
20
  RESULT="$DIR/.review/$BASE.result"
21
21
  LOG=/tmp/redline-$BASE.log
22
22
 
23
- # Kick off the review in the background and open it in the user's real browser.
24
- __REDLINE_BIN__ "$FILE" --open > "$LOG" 2>&1 &
23
+ # Kick off the review detached from this short-lived shell and open it in the user's real browser.
24
+ nohup __REDLINE_BIN__ "$FILE" --open > "$LOG" 2>&1 < /dev/null &
25
25
 
26
26
  # Step 1: wait for startup, read the URL.
27
27
  for i in $(seq 1 60); do [ -f "$STARTUP" ] && break; sleep 0.5; done
@@ -46,7 +46,7 @@ The startup file at `.review/<basename>.startup.json` is written synchronously w
46
46
  In practice, run the script above as **two separate shell calls** so you can tell the human the URL between steps:
47
47
  1. First call: everything through `echo "REDLINE_PID: $PID"`. Returns in ~1s with the URL and PID on stdout.
48
48
  2. Tell the human Redline opened in their browser, and include the URL only as a fallback (see "Surfacing the URL" below).
49
- 3. Second call: `__REDLINE_BIN__ author-wait "$FILE"`. It returns either `{ "kind": "author-needed", ... }` or `{ "kind": "result", ... }`. Long timeout (`timeout: 1800000` = 30 min, or longer). If it returns author-needed JSON, answer with `author-reply`, then run `author-wait` again.
49
+ 3. Second call: `__REDLINE_BIN__ author-wait "$FILE"`. It returns `{ "kind": "author-needed", ... }`, `{ "kind": "result", ... }`, or `{ "kind": "session-ended", ... }`. Long timeout (`timeout: 1800000` = 30 min, or longer). If it returns author-needed JSON, answer with `author-reply`, then run `author-wait` again. If it returns session-ended, inspect `$LOG` and relaunch only after explaining the failed session to the human.
50
50
 
51
51
  If invocation fails (binary missing, startup file never appears, etc.), surface the error verbatim and stop — do not try to recover. The human will re-run `redline install-skill`.
52
52
 
@@ -86,7 +86,7 @@ The full loop, when you are the outer agent producing the doc:
86
86
  2. Tell the human in one sentence what's about to happen.
87
87
  3. First shell call: launch `__REDLINE_BIN__ <abs-path> --context "<one-liner>" --open` in the background and poll for `.startup.json`. Returns in ~1s with the URL.
88
88
  4. Tell the human Redline opened in their browser and include the URL only as a fallback.
89
- 5. Second shell call: run `__REDLINE_BIN__ author-wait "$FILE"`. If it returns `kind: "author-needed"`, answer with `__REDLINE_BIN__ author-reply "$FILE" <comment-id> --message "..."`, then run `author-wait` again. Do not start unrelated work while the session runs.
89
+ 5. Second shell call: run `__REDLINE_BIN__ author-wait "$FILE"`. If it returns `kind: "author-needed"`, answer with `__REDLINE_BIN__ author-reply "$FILE" <comment-id> --message "..."`, then run `author-wait` again. If it returns `kind: "session-ended"`, inspect the log path from step 1 and tell the human the session died instead of silently relaunching. Do not start unrelated work while the session runs.
90
90
  6. On `approved`: re-read the file from disk (it may have been revised) and continue with whatever required sign-off.
91
91
  7. On `abandoned` or `error`: stop and ask the human how to proceed; do not retry automatically.
92
92
 
@@ -66,7 +66,8 @@ export interface AuthorReplyResult {
66
66
 
67
67
  export type AuthorWaitResult =
68
68
  | { kind: "author-needed"; file: string; author_needed: AuthorNeededItem[] }
69
- | { kind: "result"; file: string; result: Record<string, unknown> };
69
+ | { kind: "result"; file: string; result: Record<string, unknown> }
70
+ | { kind: "session-ended"; file: string; pid: number; message: string };
70
71
 
71
72
  function startupPath(filePath: string): string {
72
73
  return path.join(path.dirname(filePath), ".review", path.basename(filePath) + ".startup.json");
@@ -76,16 +77,27 @@ function resultPath(filePath: string): string {
76
77
  return path.join(path.dirname(filePath), ".review", path.basename(filePath) + ".result");
77
78
  }
78
79
 
79
- function readStartup(filePath: string): { url?: string; csrf_token?: string } | null {
80
+ function readStartup(filePath: string): { url?: string; csrf_token?: string; pid?: number } | null {
80
81
  const sp = startupPath(filePath);
81
82
  if (!existsSync(sp)) return null;
82
83
  try {
83
- return JSON.parse(readFileSync(sp, "utf-8")) as { url?: string; csrf_token?: string };
84
+ return JSON.parse(readFileSync(sp, "utf-8")) as { url?: string; csrf_token?: string; pid?: number };
84
85
  } catch {
85
86
  return null;
86
87
  }
87
88
  }
88
89
 
90
+ function processIsAlive(pid: number): boolean {
91
+ try {
92
+ process.kill(pid, 0);
93
+ return true;
94
+ } catch (e) {
95
+ const code = (e as { code?: string }).code;
96
+ // EPERM means the process exists but this user cannot signal it.
97
+ return code === "EPERM";
98
+ }
99
+ }
100
+
89
101
  async function postToLiveServer(
90
102
  filePath: string,
91
103
  commentId: string,
@@ -183,6 +195,16 @@ export async function waitForAuthorEvent(
183
195
  return { kind: "result", file: filePath, result: JSON.parse(raw) as Record<string, unknown> };
184
196
  }
185
197
 
198
+ const startup = readStartup(filePath);
199
+ if (typeof startup?.pid === "number" && !processIsAlive(startup.pid)) {
200
+ return {
201
+ kind: "session-ended",
202
+ file: filePath,
203
+ pid: startup.pid,
204
+ message: "Redline session process ended before writing a result.",
205
+ };
206
+ }
207
+
186
208
  if (deadline != null && Date.now() >= deadline) {
187
209
  throw new Error("Timed out waiting for author-needed comments or review result");
188
210
  }