jeo-code 0.6.35 → 0.6.37

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
@@ -6,6 +6,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.6.37] - 2026-06-20
10
+ _Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot._
11
+
12
+ ### Fixed
13
+ - **The boxed prompt's ↑/↓ recalls input history again on a soft-wrapped line.** A long single line that the box wraps to several visual rows is no longer treated as multi-line: ↑/↓ fall through to readline's history recall (the dominant REPL expectation) instead of dragging the wrapped tail up a visual row. In-box vertical caret nav (textarea feel) is now gated behind a GENUINE multi-line draft — one carrying an explicit Shift+Enter / pasted break, stored as the private-use `MULTILINE_SENTINEL` — and still yields the arrows to an open slash list or the Ctrl+O history panel. New `isGenuineMultilineDraft` / `shouldBoxVerticalNav` helpers make the gate unit-testable independent of the live readline/PTY wiring.
14
+ - **Every terminating Spec-first workflow stage now surfaces a user-visible answer.** Three stages could previously reach a terminal state with no message explaining the outcome:
15
+ - **`team`** routes all subagent output through the engine `log()`/`io.output` sink (zero raw `console.log` in `executeTaskWithAgent`) and prints a `<role> report:` header followed by every line of the subagent's reason on success.
16
+ - **`deep-interview`** gates its `[Handoff Ready]` / `onProgress(complete)` signal on a real frozen seed: `freezeSeed` now returns `Promise<boolean>`, and a freeze failure emits `[HOLD]` and keeps the interview open instead of falsely claiming the requirement is crystallized.
17
+ - **`ralplan`** reports a discarded revision: an invalid `[ITERATE]` revision now logs `discarding the revision; the [ITERATE] verdict stands` instead of silently surfacing the stale verdict.
18
+
19
+ ### Verified
20
+ - `scripts/mem-probe.ts` (2000 LaunchTui turns) shows a flat post-GC heap — per-turn slope **−556 bytes/turn**, returning to its settled floor — with a single `exit` listener and zero `process` SIGINT/listener accumulation; `scripts/tmux-verify.sh smoke` boots `jeo --tmux` to a clean input box + model bar (EXIT 0). `bun run typecheck` is clean and `bun test` is **1751 pass / 0 fail** across 216 files, including the new `test/box-vertical-nav.test.ts` and the no-answer-deadend targeted suite (`team-run`/`team-schema`/`team-subagent`/`deep-interview`/`deep-interview-noninteractive`/`workflow-integrity`/`approve`/`parse-role-gate-verdict` → 71 pass / 0 fail).
21
+
22
+
23
+ ## [0.6.36] - 2026-06-20
24
+ _When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot._
25
+
26
+ ### Fixed
27
+ - **A `--tmux` mouse drag-select now reaches the system clipboard instead of vanishing.** Turning the mouse on (so on-screen text can be selected) re-routes a drag into tmux copy-mode, where the selection used to die inside tmux's own buffer — `cmd/ctrl+v` got nothing. The launch path now applies the same clipboard repair to the CURRENT session that `tmuxProfileCommands` applies to jeo-owned sessions: new `currentTmuxClipboardCommands(env, deps)` emits `set-option set-clipboard on` (lets the copy-mode selection escape via OSC 52) and, when a local clipboard tool is on PATH, `set-option copy-command <tool>` (pipes the drag-select straight to `pbcopy` / `wl-copy` / `xclip` / `xsel` / `clip` for terminals that don't honor OSC 52). Both are written **session-locally — never `-t` or `-g`** — so the user's other tmux sessions are untouched; `JEO_TMUX_PROFILE=0` opts out, and `copy-command` is skipped when no tool is found.
28
+
29
+ ### Changed
30
+ - **`/help` documents the drag-to-copy behavior and its escape hatch.** Two hotkey rows now explain that a drag selects on-screen text (copies on `cmd/ctrl+c`, and auto-copies to the system clipboard under `--tmux`) and that **Shift-drag** (iTerm/macOS: **Option-drag**) forces the terminal's own selection when tmux owns the mouse.
31
+
32
+ ### Verified
33
+ - `scripts/mem-probe.ts` (2000 LaunchTui turns) shows a flat post-GC heap — per-turn slope **−541 bytes/turn**, returning to its settled floor — with zero `process` SIGINT/listener accumulation; `scripts/tmux-verify.sh smoke` boots `jeo --tmux` to a clean input box + model bar (EXIT 0). `bun run typecheck` clean and `bun test` **1748 pass / 0 fail** across 215 files, including the new `currentTmuxClipboardCommands` session-local / no-tool / opt-out cases in `test/tmux.test.ts`.
34
+
35
+
9
36
  ## [0.6.35] - 2026-06-20
10
37
  _The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check._
11
38
 
package/README.ja.md CHANGED
@@ -200,11 +200,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
200
200
  ## 変更履歴 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
204
+ - **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
203
205
  - **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
204
206
  - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
205
207
  - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
206
- - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
207
- - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -200,11 +200,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
200
200
  ## 변경 이력 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
204
+ - **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
203
205
  - **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
204
206
  - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
205
207
  - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
206
- - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
207
- - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -200,11 +200,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
200
200
  ## Changelog
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
204
+ - **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
203
205
  - **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
204
206
  - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
205
207
  - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
206
- - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
207
- - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -200,11 +200,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
200
200
  ## 更新日志 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
204
+ - **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
203
205
  - **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
204
206
  - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
205
207
  - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
206
- - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
207
- - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.35",
3
+ "version": "0.6.37",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -567,7 +567,7 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
567
567
  let ambiguity = state.current_ambiguity ?? 1.0;
568
568
  let lastParsed: SocraticResponse | undefined;
569
569
 
570
- const freezeSeed = async (parsed: SocraticResponse): Promise<void> => {
570
+ const freezeSeed = async (parsed: SocraticResponse): Promise<boolean> => {
571
571
  const readiness = freezeReadiness(parsed);
572
572
  if (!readiness.ok) throw new Error(`Refusing to freeze seed: ${readiness.reason}.`);
573
573
 
@@ -594,7 +594,7 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
594
594
  `[ERROR] Seed round-trip self-check FAILED — the acceptance criteria would not survive ultragoal's parser ` +
595
595
  `(writer/parser drift). NOT freezing the seed. Got back: ${JSON.stringify(parsedBack)}`,
596
596
  );
597
- return;
597
+ return false;
598
598
  }
599
599
  await fs.writeFile(seedPath, seedContent, "utf-8");
600
600
  state!.current_phase = "complete";
@@ -603,6 +603,7 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
603
603
  state!.current_ambiguity = Math.min(state!.current_ambiguity ?? threshold, threshold);
604
604
  await writeWorkflowState("deep-interview", state!, cwd);
605
605
  log(`Saved frozen requirements spec seed to: ${seedPath}`);
606
+ return true;
606
607
  };
607
608
 
608
609
  while (round <= 10) {
@@ -629,12 +630,20 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
629
630
  const readiness = freezeReadiness(parsed);
630
631
  if (ambiguity <= threshold && readiness.ok) {
631
632
  log(`\n[SUCCESS] Ambiguity is <= ${(threshold * 100).toFixed(0)}%! Concluding requirements gather.`);
632
- await freezeSeed(parsed);
633
- log("\n[Handoff Ready] Requirement is crystallized. Next, run 'jeo ralplan' to build a plan.");
634
- if (opts.onProgress) {
635
- opts.onProgress({ skill: "deep-interview", phase: "complete" });
633
+ const frozen = await freezeSeed(parsed);
634
+ if (frozen) {
635
+ log("\n[Handoff Ready] Requirement is crystallized. Next, run 'jeo ralplan' to build a plan.");
636
+ if (opts.onProgress) {
637
+ opts.onProgress({ skill: "deep-interview", phase: "complete" });
638
+ }
639
+ break;
636
640
  }
637
- break;
641
+ // Freeze failed (e.g. round-trip self-check): do NOT emit a false
642
+ // "crystallized" handoff. Fall through to the normal question loop
643
+ // below — it already asks the follow-up, pushes history, and bumps
644
+ // the round — so the interview simply stays open.
645
+ log("\n[HOLD] Could not freeze the requirements seed — see the error above. Keeping the interview open.");
646
+
638
647
  }
639
648
 
640
649
  let nextQuestion = parsed.nextQuestion?.trim() || interviewLanguage.acceptanceFollowup;
@@ -51,6 +51,33 @@ export function isStandaloneBackspace(chunk: string): boolean {
51
51
  return chunk.length > 0 && /^[\x7f\b]+$/.test(chunk);
52
52
  }
53
53
 
54
+ /** Private-use sentinel the input filter substitutes for an EXPLICIT line break
55
+ * (Shift+Enter / a pasted newline) before the bytes reach readline, so the draft can
56
+ * carry hard newlines through readline's single-line buffer. A line that merely
57
+ * SOFT-WRAPS at the box width contains NO sentinel — that distinction is the whole
58
+ * point of `isGenuineMultilineDraft`. */
59
+ export const MULTILINE_SENTINEL = "\uE000";
60
+
61
+ /** True when the draft has at least one EXPLICIT line break (a sentinel) — i.e. the
62
+ * user deliberately made it multi-line. A long single line that the box soft-wraps to
63
+ * several visual rows is NOT multi-line and returns false. */
64
+ export function isGenuineMultilineDraft(line: string): boolean {
65
+ return line.includes(MULTILINE_SENTINEL);
66
+ }
67
+
68
+ /** Whether an Up/Down keystroke should move the caret BETWEEN the box's visual rows
69
+ * (textarea feel) instead of falling through to readline's input-history recall. True
70
+ * only for a genuinely multi-line draft with no slash dropdown or history panel owning
71
+ * the arrows. A soft-wrapped one-liner returns false, so ↑ recalls the previous prompt
72
+ * rather than dragging the wrapped tail up a visual row. */
73
+ export function shouldBoxVerticalNav(
74
+ line: string,
75
+ opts: { slashMatchCount: number; historyPanelOpen: boolean },
76
+ ): boolean {
77
+ return isGenuineMultilineDraft(line) && opts.slashMatchCount === 0 && !opts.historyPanelOpen;
78
+ }
79
+
80
+
54
81
  /**
55
82
  * macOS / fixterms combo-key normalization for the boxed prompt's line editor.
56
83
  *
@@ -23,6 +23,8 @@ export function hotkeysLines(): string[] {
23
23
  " @path mention a file (Tab completes relative paths)",
24
24
  " Ctrl-V paste a copied image from the clipboard into the next message",
25
25
  " drag-drop drop an image file onto the box to attach it (its path becomes [image #N])",
26
+ " drag select on-screen text to copy — copies on cmd/ctrl+c; under --tmux a drag auto-copies to the system clipboard",
27
+ " Shift-drag force the terminal's own selection when tmux owns the mouse (iTerm/macOS: Option-drag)",
26
28
  ];
27
29
  }
28
30
 
@@ -106,8 +106,8 @@ export function shellQuote(arg: string): string {
106
106
  */
107
107
  export function shouldEnableCurrentTmuxMouse(env: Record<string, string | undefined>): boolean {
108
108
  return !!env.TMUX
109
- && (env.JEO_TMUX_LAUNCHED ?? env.JEO_TMUX_LAUNCHED) !== "1"
110
- && (env.JEO_TMUX_MOUSE ?? env.JEO_TMUX_MOUSE) !== "0";
109
+ && env.JEO_TMUX_LAUNCHED !== "1"
110
+ && env.JEO_TMUX_MOUSE !== "0";
111
111
  }
112
112
 
113
113
  /**
@@ -160,7 +160,7 @@ export function tmuxProfileCommands(
160
160
  ): TmuxProfileCommand[] {
161
161
  const t = `=${target}:`;
162
162
  const commands: TmuxProfileCommand[] = [];
163
- if ((env.JEO_TMUX_MOUSE ?? env.JEO_TMUX_MOUSE) !== "0") {
163
+ if (env.JEO_TMUX_MOUSE !== "0") {
164
164
  commands.push({
165
165
  description: "enable tmux mouse scrolling (wheel-up → copy-mode over real history)",
166
166
  args: ["set-option", "-t", t, "mouse", "on"],
@@ -182,7 +182,7 @@ export function tmuxProfileCommands(
182
182
  args: ["set-option", "-t", t, "@jeo-project", meta.project],
183
183
  });
184
184
  }
185
- if ((env.JEO_TMUX_PROFILE ?? env.JEO_TMUX_PROFILE) !== "0") {
185
+ if (env.JEO_TMUX_PROFILE !== "0") {
186
186
  commands.push(
187
187
  {
188
188
  description: "enable tmux clipboard integration",
@@ -210,6 +210,40 @@ export function tmuxProfileCommands(
210
210
  return commands;
211
211
  }
212
212
 
213
+ /**
214
+ * Clipboard set-options for the CURRENT (foreign) tmux session that the in-session
215
+ * `jeo --tmux` path turns `mouse on` for. Enabling the mouse re-routes a plain
216
+ * drag into copy-mode, so without these a drag-select no longer lands anywhere:
217
+ * - `set-clipboard on` lets the copy-mode selection reach the outer terminal via OSC52;
218
+ * - `copy-command` pipes that selection straight to the local clipboard tool
219
+ * (pbcopy / wl-copy / xclip / xsel / clip), so a drag-select copies for `cmd+v`
220
+ * even where OSC52 is not honored.
221
+ * Applied WITHOUT `-t` (the current session only — never -g, so the user's other
222
+ * sessions are untouched). `JEO_TMUX_PROFILE=0` opts out; `copy-command` is skipped
223
+ * when no clipboard tool is on PATH. This is the in-session analogue of the
224
+ * clipboard block in {@link tmuxProfileCommands} for jeo-owned sessions.
225
+ */
226
+ export function currentTmuxClipboardCommands(
227
+ env: Record<string, string | undefined>,
228
+ deps: { platform?: NodeJS.Platform; which?: (bin: string) => string | null } = {},
229
+ ): TmuxProfileCommand[] {
230
+ if (env.JEO_TMUX_PROFILE === "0") return [];
231
+ const commands: TmuxProfileCommand[] = [
232
+ {
233
+ description: "enable tmux clipboard integration",
234
+ args: ["set-option", "set-clipboard", "on"],
235
+ },
236
+ ];
237
+ const copyCmd = tmuxCopyCommand(deps.platform ?? process.platform, deps.which ?? ((bin: string) => Bun.which(bin)));
238
+ if (copyCmd) {
239
+ commands.push({
240
+ description: "pipe copy-mode selection to the system clipboard",
241
+ args: ["set-option", "copy-command", copyCmd],
242
+ });
243
+ }
244
+ return commands;
245
+ }
246
+
213
247
  /**
214
248
  * Resolve a git worktree path (gjc `--worktree <path>` parity). If the path
215
249
  * already exists it is reused as-is; otherwise a new worktree is created on a
@@ -118,6 +118,7 @@ import {
118
118
  shouldEnableCurrentTmuxMouse,
119
119
  tmuxLaunchCommand,
120
120
  tmuxProfileCommands,
121
+ currentTmuxClipboardCommands,
121
122
  resolveWorktree,
122
123
  shellQuote,
123
124
  type TmuxCreateResult,
@@ -135,6 +136,9 @@ import {
135
136
  matchTerminalReport,
136
137
  stripMouseReports,
137
138
  rewriteCursorCombos,
139
+ MULTILINE_SENTINEL,
140
+ isGenuineMultilineDraft,
141
+ shouldBoxVerticalNav,
138
142
  queuePromptInputChunk,
139
143
  captureLivePromptInputChunk,
140
144
  restoreQueuedLinesToPrefill,
@@ -186,6 +190,7 @@ export {
186
190
  shouldEnableCurrentTmuxMouse,
187
191
  tmuxLaunchCommand,
188
192
  tmuxProfileCommands,
193
+ currentTmuxClipboardCommands,
189
194
  resolveWorktree,
190
195
  shellQuote,
191
196
  type TmuxCreateResult,
@@ -201,6 +206,9 @@ export {
201
206
  matchTerminalReport,
202
207
  stripMouseReports,
203
208
  rewriteCursorCombos,
209
+ MULTILINE_SENTINEL,
210
+ isGenuineMultilineDraft,
211
+ shouldBoxVerticalNav,
204
212
  queuePromptInputChunk,
205
213
  captureLivePromptInputChunk,
206
214
  restoreQueuedLinesToPrefill,
@@ -420,6 +428,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
420
428
  const tmuxBin = Bun.which("tmux");
421
429
  if (tmuxBin) {
422
430
  try { Bun.spawnSync([tmuxBin, "set-option", "mouse", "on"]); } catch { /* best-effort */ }
431
+ // Enabling the mouse re-routes a drag into copy-mode; set-clipboard +
432
+ // copy-command make that drag-select actually land on the system clipboard.
433
+ for (const c of currentTmuxClipboardCommands(process.env)) {
434
+ try { Bun.spawnSync([tmuxBin, ...c.args]); } catch { /* best-effort */ }
435
+ }
423
436
  }
424
437
  }
425
438
  }
@@ -1353,7 +1366,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1353
1366
  // readline sees them — readline inserts it as an ordinary character (no per-line
1354
1367
  // submit/race), the box renders it as a real line break, and it is expanded back to
1355
1368
  // "\n" on submit. On for any interactive TTY; JEO_NO_MULTILINE=1 reads stdin directly.
1356
- const SENTINEL = "\uE000";
1369
+ const SENTINEL = MULTILINE_SENTINEL;
1357
1370
  const SHIFT_ENTER_SEQS = ["\u001b[27;2;13~", "\u001b[13;2u"];
1358
1371
  // Multi-line input filter is ON for any interactive TTY: reliable multi-line paste
1359
1372
  // (fills the box, submits intact into the user card) is the default. The lone-"\n"
@@ -1431,14 +1444,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1431
1444
  // Option/Cmd+Backspace) into the canonical control bytes it DOES act on.
1432
1445
  const combo = matchCursorCombo(data, i);
1433
1446
  if (combo) { out += combo[1]; i += combo[0].length; continue; }
1434
- // Up/Down inside a multi-line / wrapped draft move the caret between the box's
1435
- // visual rows (textarea feel). Only when no slash list or history panel owns ↑/↓,
1436
- // and only away from the top/bottom edge at the edge the keys fall through to
1437
- // readline so ↑/↓ still recalls input history.
1447
+ // Up/Down inside a GENUINELY multi-line draft (explicit Shift+Enter breaks
1448
+ // SENTINEL) move the caret between the box's visual rows (textarea feel). A
1449
+ // single line that merely SOFT-WRAPS is NOT treated as multi-line: ↑/↓ fall
1450
+ // through to readline so they recall input history — the dominant REPL
1451
+ // expectation. (Without this gate, ↑ on a wrapped one-liner jumped the caret up
1452
+ // a visual row instead of recalling the previous prompt, so the last wrapped
1453
+ // word appeared to "follow the caret up".) Skipped when a slash list or history
1454
+ // panel owns ↑/↓, and at the top/bottom edge the keys still reach history.
1438
1455
  if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
1439
1456
  const dir = data[i + 2] === "A" ? "up" : "down";
1440
1457
  const line = activeRl?.line ?? "";
1441
- if (line.length > 0 && navMatches.length === 0 && promptHistoryLines == null && activeRl) {
1458
+ if (shouldBoxVerticalNav(line, { slashMatchCount: navMatches.length, historyPanelOpen: promptHistoryLines != null }) && activeRl) {
1442
1459
  const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
1443
1460
  const textWidth = Math.max(1, Math.max(24, winCols) - 6);
1444
1461
  const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
@@ -2579,6 +2596,35 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2579
2596
  return;
2580
2597
  }
2581
2598
  if (!previewArmed || pickerActive) return;
2599
+ // Drag-and-drop file attach: a terminal delivers a dropped file as its PATH typed
2600
+ // into the box, wrapped in a bracketed paste. On paste-end, swap any readable image
2601
+ // path LIVE for the same `[image #N]` tag Ctrl+V uses, so the box shows the tag
2602
+ // instead of a long raw filesystem path. Non-image / unreadable paths and ordinary
2603
+ // text pastes match nothing and are a no-op. Submit-time attach (below) still covers
2604
+ // terminals that deliver a drop WITHOUT bracketing it.
2605
+ if (key?.name === "paste-end" && !pasteInFlight && !pasteLineFired) {
2606
+ pasteInFlight = true;
2607
+ // Defer one tick: the dropped bytes reach readline through the keyFilter
2608
+ // PassThrough, so rl.line is only settled after the current stream turn.
2609
+ setImmediate(() => {
2610
+ void (async () => {
2611
+ try {
2612
+ const rli = rl as unknown as { line: string; cursor: number };
2613
+ const before = rli.line;
2614
+ const dropped = await attachImagePaths(before, pendingImages.length + 1);
2615
+ if (dropped.images.length === 0 || dropped.text === before) return;
2616
+ pendingImages.push(...dropped.images);
2617
+ rli.line = dropped.text;
2618
+ rli.cursor = dropped.text.length; // drop lands at the caret (line end in the common case)
2619
+ typedLine = dropped.text;
2620
+ if (previewArmed) drawFooter(previewLines(typedLine, navIdx));
2621
+ } finally {
2622
+ pasteInFlight = false;
2623
+ }
2624
+ })();
2625
+ });
2626
+ return;
2627
+ }
2582
2628
  // Ctrl+L: redraw / re-anchor the prompt. The recovery for a footer whose in-place
2583
2629
  // anchor drifted after the screen scrolled (typed text stops showing in the box).
2584
2630
  if (key?.ctrl && key.name === "l") {
@@ -257,6 +257,12 @@ export async function runRalplanEngine(opts: RalplanEngineOptions = {}): Promise
257
257
  cleanPlan = revised;
258
258
  await fs.writeFile(planPath, cleanPlan, "utf-8");
259
259
  gate = await runConsensusCriticGate({ cwd, seedContent, plan: cleanPlan, signal: opts.signal });
260
+ } else {
261
+ // The revision did not parse as a schema/role-valid plan, so it cannot be
262
+ // re-gated — the original [ITERATE] verdict stands. Report this explicitly
263
+ // instead of silently discarding the revision attempt (which otherwise
264
+ // surfaces only as the unchanged "verdict: ITERATE" failure below).
265
+ log(`[ralplan] The revised plan was not schema/role-valid — discarding the revision; the [ITERATE] verdict stands.`);
260
266
  }
261
267
  }
262
268
  ralplanState.plan_path = planPath;
@@ -347,6 +347,7 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
347
347
  cwd,
348
348
  roleId: roleByIndex[activeIndex],
349
349
  strictMutations: opts.strictMutations ?? false,
350
+ log,
350
351
  });
351
352
 
352
353
  if (opts.signal?.aborted) {
@@ -393,13 +394,14 @@ export async function runTeamCommand(args: string[] = []): Promise<void> {
393
394
  }
394
395
  }
395
396
 
396
- async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string; strictMutations?: boolean }): Promise<boolean> {
397
+ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string; strictMutations?: boolean; log: (line: string) => void }): Promise<boolean> {
398
+ const log = ctx.log;
397
399
  const config = await readGlobalConfig();
398
400
  const role = getSubagentRole(ctx.roleId, config) ?? defaultSubagentRole();
399
401
  const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
400
402
  const model = resolveSubagentModel(role.id, config);
401
403
  const maxSteps = resolveSubagentMaxSteps(role.id, config);
402
- console.log(` └─ Subagent: ${role.title} · model ${model} · ≤${maxSteps} steps`);
404
+ log(` └─ Subagent: ${role.title} · model ${model} · ≤${maxSteps} steps`);
403
405
 
404
406
  const contextTokens = catalogMetadata(model)?.contextTokens;
405
407
 
@@ -428,13 +430,13 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
428
430
  events: {
429
431
  onAssistant: (_raw, invocation) => {
430
432
  if (!invocation) {
431
- console.log(formatRalphStreamEvent("error", "invalid tool-call json; retrying", renderOpts));
433
+ log(formatRalphStreamEvent("error", "invalid tool-call json; retrying", renderOpts));
432
434
  } else if (invocation.tool !== "done") {
433
- console.log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`, renderOpts));
435
+ log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`, renderOpts));
434
436
  }
435
437
  },
436
438
  onStep: async step => {
437
- console.log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`, renderOpts));
439
+ log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`, renderOpts));
438
440
  try {
439
441
  await maybeCompact(history, { model, contextTokens });
440
442
  } catch (err) {
@@ -446,27 +448,27 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
446
448
  if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
447
449
  else if (tool === "bash") bashRuns++;
448
450
  }
449
- console.log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
451
+ log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
450
452
  },
451
- onNotice: msg => console.log(formatRalphStreamEvent("step", msg, renderOpts)),
453
+ onNotice: msg => log(formatRalphStreamEvent("step", msg, renderOpts)),
452
454
  },
453
455
  });
454
456
 
455
457
  const reason = result.doneReason?.trim() || `${role.title} did not converge within ${result.steps} steps`;
456
458
  if (!result.done) {
457
- console.log(formatRalphStreamEvent("error", reason, renderOpts));
459
+ log(formatRalphStreamEvent("error", reason, renderOpts));
458
460
  return false;
459
461
  }
460
462
 
461
463
  const contract = validateSubagentDoneReason(role, reason);
462
464
  if (!contract.ok) {
463
- console.log(formatRalphStreamEvent("error", `${role.title} report incomplete: missing ${contract.missing?.join(", ")}`, renderOpts));
465
+ log(formatRalphStreamEvent("error", `${role.title} report incomplete: missing ${contract.missing?.join(", ")}`, renderOpts));
464
466
  return false;
465
467
  }
466
468
 
467
469
  const gate = parseRoleGateVerdict(role.id, reason);
468
470
  if (!gate.ok) {
469
- console.log(formatRalphStreamEvent("error", gate.message ?? `${role.title} blocked execution`, renderOpts));
471
+ log(formatRalphStreamEvent("error", gate.message ?? `${role.title} blocked execution`, renderOpts));
470
472
  return false;
471
473
  }
472
474
 
@@ -483,11 +485,17 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
483
485
  const hardFail = ctx.strictMutations && bashRuns === 0;
484
486
  // Round-12: separate the tones so a passing advisory run doesn't masquerade
485
487
  // as a stream:error — only a real hard-fail is red; an advisory note is warn.
486
- console.log(formatRalphStreamEvent(hardFail ? "error" : "warn", msg, renderOpts));
488
+ log(formatRalphStreamEvent(hardFail ? "error" : "warn", msg, renderOpts));
487
489
  if (hardFail) {
488
490
  return false;
489
491
  }
490
492
  }
491
- console.log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
493
+ // Surface the subagent's ACTUAL report to the user — not just a "finished"
494
+ // status. Previously `reason` was consumed only for validation/gating and the
495
+ // report content was discarded, so `team` runs produced no visible answer.
496
+ log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
497
+ log(`\n${role.title} report:`);
498
+ log(reason); // log() already splits multi-line strings across the io.output sink
499
+
492
500
  return true;
493
501
  }