jeo-code 0.6.36 → 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 +14 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/commands/deep-interview.ts +16 -7
- package/src/commands/launch/input.ts +27 -0
- package/src/commands/launch.ts +45 -6
- package/src/commands/ralplan.ts +6 -0
- package/src/commands/team.ts +20 -12
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ 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
|
+
|
|
9
23
|
## [0.6.36] - 2026-06-20
|
|
10
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._
|
|
11
25
|
|
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.
|
|
203
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.
|
|
204
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.
|
|
205
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.
|
|
206
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.
|
|
207
|
-
- **[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.
|
|
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.
|
|
203
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.
|
|
204
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.
|
|
205
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.
|
|
206
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.
|
|
207
|
-
- **[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.
|
|
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.
|
|
203
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.
|
|
204
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.
|
|
205
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.
|
|
206
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.
|
|
207
|
-
- **[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.
|
|
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.
|
|
203
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.
|
|
204
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.
|
|
205
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.
|
|
206
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.
|
|
207
|
-
- **[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.
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
opts.onProgress
|
|
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
|
-
|
|
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
|
*
|
package/src/commands/launch.ts
CHANGED
|
@@ -136,6 +136,9 @@ import {
|
|
|
136
136
|
matchTerminalReport,
|
|
137
137
|
stripMouseReports,
|
|
138
138
|
rewriteCursorCombos,
|
|
139
|
+
MULTILINE_SENTINEL,
|
|
140
|
+
isGenuineMultilineDraft,
|
|
141
|
+
shouldBoxVerticalNav,
|
|
139
142
|
queuePromptInputChunk,
|
|
140
143
|
captureLivePromptInputChunk,
|
|
141
144
|
restoreQueuedLinesToPrefill,
|
|
@@ -203,6 +206,9 @@ export {
|
|
|
203
206
|
matchTerminalReport,
|
|
204
207
|
stripMouseReports,
|
|
205
208
|
rewriteCursorCombos,
|
|
209
|
+
MULTILINE_SENTINEL,
|
|
210
|
+
isGenuineMultilineDraft,
|
|
211
|
+
shouldBoxVerticalNav,
|
|
206
212
|
queuePromptInputChunk,
|
|
207
213
|
captureLivePromptInputChunk,
|
|
208
214
|
restoreQueuedLinesToPrefill,
|
|
@@ -1360,7 +1366,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1360
1366
|
// readline sees them — readline inserts it as an ordinary character (no per-line
|
|
1361
1367
|
// submit/race), the box renders it as a real line break, and it is expanded back to
|
|
1362
1368
|
// "\n" on submit. On for any interactive TTY; JEO_NO_MULTILINE=1 reads stdin directly.
|
|
1363
|
-
const SENTINEL =
|
|
1369
|
+
const SENTINEL = MULTILINE_SENTINEL;
|
|
1364
1370
|
const SHIFT_ENTER_SEQS = ["\u001b[27;2;13~", "\u001b[13;2u"];
|
|
1365
1371
|
// Multi-line input filter is ON for any interactive TTY: reliable multi-line paste
|
|
1366
1372
|
// (fills the box, submits intact into the user card) is the default. The lone-"\n"
|
|
@@ -1438,14 +1444,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1438
1444
|
// Option/Cmd+Backspace) into the canonical control bytes it DOES act on.
|
|
1439
1445
|
const combo = matchCursorCombo(data, i);
|
|
1440
1446
|
if (combo) { out += combo[1]; i += combo[0].length; continue; }
|
|
1441
|
-
// Up/Down inside a multi-line
|
|
1442
|
-
// visual rows (textarea feel).
|
|
1443
|
-
//
|
|
1444
|
-
// readline so
|
|
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.
|
|
1445
1455
|
if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
|
|
1446
1456
|
const dir = data[i + 2] === "A" ? "up" : "down";
|
|
1447
1457
|
const line = activeRl?.line ?? "";
|
|
1448
|
-
if (line
|
|
1458
|
+
if (shouldBoxVerticalNav(line, { slashMatchCount: navMatches.length, historyPanelOpen: promptHistoryLines != null }) && activeRl) {
|
|
1449
1459
|
const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
1450
1460
|
const textWidth = Math.max(1, Math.max(24, winCols) - 6);
|
|
1451
1461
|
const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
|
|
@@ -2586,6 +2596,35 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2586
2596
|
return;
|
|
2587
2597
|
}
|
|
2588
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
|
+
}
|
|
2589
2628
|
// Ctrl+L: redraw / re-anchor the prompt. The recovery for a footer whose in-place
|
|
2590
2629
|
// anchor drifted after the screen scrolled (typed text stops showing in the box).
|
|
2591
2630
|
if (key?.ctrl && key.name === "l") {
|
package/src/commands/ralplan.ts
CHANGED
|
@@ -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;
|
package/src/commands/team.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
433
|
+
log(formatRalphStreamEvent("error", "invalid tool-call json; retrying", renderOpts));
|
|
432
434
|
} else if (invocation.tool !== "done") {
|
|
433
|
-
|
|
435
|
+
log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`, renderOpts));
|
|
434
436
|
}
|
|
435
437
|
},
|
|
436
438
|
onStep: async step => {
|
|
437
|
-
|
|
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
|
-
|
|
451
|
+
log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
|
|
450
452
|
},
|
|
451
|
-
onNotice: msg =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
488
|
+
log(formatRalphStreamEvent(hardFail ? "error" : "warn", msg, renderOpts));
|
|
487
489
|
if (hardFail) {
|
|
488
490
|
return false;
|
|
489
491
|
}
|
|
490
492
|
}
|
|
491
|
-
|
|
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
|
}
|