@aragon-agent/cli 0.6.26 → 0.6.27

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
@@ -77,6 +77,21 @@ were written.
77
77
 
78
78
  ### Fixed
79
79
 
80
+ - **`Ctrl+L` opened a window that failed with a PowerShell `ParserError` on
81
+ Windows.** The script handed to `-EncodedCommand` was the argv joined with
82
+ quotes but with no call operator, and PowerShell reads a quoted string in command
83
+ position as an *expression*: with node installed under `C:\Program Files\...`
84
+ the child was never created - `UnexpectedToken` at column 36 - while the parent
85
+ TUI still reported that the window opened. `powerShellScript` now emits
86
+ `& <argv...>`, which is the only form that runs a word of any shape as a
87
+ command. The failure fallback follows: it is now two labelled lines
88
+ (`PowerShell:` / `cmd.exe :`), because one line cannot be correct for both
89
+ parsers - and the `PowerShell:` line is byte-for-byte the script the window
90
+ receives. A leading `@` can no longer be left bare either, since `@name` is
91
+ PowerShell's splat operator and silently swallows an argument. New
92
+ `powershell-script-parse.test.ts` hands the generated scripts to the real
93
+ Windows PowerShell parser (and pins the old shape to its exact error), so this
94
+ class of defect has a guard instead of a green suite.
80
95
  - **A conversation saved mid-tool could be resumed into an illegal history.**
81
96
  A `tool_call` with no matching `tool_result` makes the next request fail. The
82
97
  checkpoint writer closes every dangling call on the copy it writes and re-checks
package/README.md CHANGED
@@ -1838,16 +1838,45 @@ The platform ladder, in order:
1838
1838
  The shell is `pwsh.exe` then `powershell.exe` on Windows, and `$SHELL` then `bash`
1839
1839
  then `/bin/sh` elsewhere. Windows receives the command as `-EncodedCommand` (base64
1840
1840
  of UTF-16LE), because that alphabet has no spaces, quotes or `cmd` metacharacters -
1841
- it survives `cmd` -> `start` -> PowerShell without a quoting argument.
1841
+ it survives `cmd` -> `start` -> PowerShell without a quoting argument. The payload
1842
+ that gets encoded is `& <argv...>`, and **the leading `&` is load-bearing**: in
1843
+ command position PowerShell reads a quoted string as an *expression*, so with a node
1844
+ install under `C:\Program Files\...` the script `'C:\Program Files\nodejs\node.exe'
1845
+ <script>` is a `ParserError ... UnexpectedToken` and no child is ever created - while
1846
+ the parent still reports that the window opened. `&` is PowerShell's call operator:
1847
+ it takes the next word as a command name whatever its shape.
1842
1848
 
1843
1849
  When no terminal can be opened - an SSH session, a container, a machine with no
1844
- graphical session - the command opens nothing and prints the fallback instead:
1850
+ graphical session - the command opens nothing and prints the fallback instead. On
1851
+ Windows that fallback is **two labelled lines**, because one line cannot be correct
1852
+ for both parsers (`& 'x'` is right for PowerShell and wrong for `cmd.exe`, which
1853
+ keeps the single quotes; `"x"` is right for `cmd.exe` and an expression to
1854
+ PowerShell):
1855
+
1856
+ ```
1857
+ No terminal window could be opened here. Run this in another terminal:
1858
+ PowerShell: & aragon --resume a4k2m9
1859
+ cmd.exe : aragon --resume a4k2m9
1860
+ ```
1861
+
1862
+ Copy the line that matches the terminal you opened. macOS and Linux keep the single
1863
+ line they have always printed:
1845
1864
 
1846
1865
  ```
1847
1866
  No terminal window could be opened here. Run this in another terminal:
1848
1867
  aragon --resume a4k2m9
1849
1868
  ```
1850
1869
 
1870
+ A `node <dist/cli.js>` build prints the same two lines with the full paths, quoted:
1871
+
1872
+ ```
1873
+ PowerShell: & 'C:\Program Files\nodejs\node.exe' C:\Users\me\AppData\Roaming\npm\node_modules\@aragon-agent\cli\dist\launcher.js --resume a4k2m9
1874
+ cmd.exe : "C:\Program Files\nodejs\node.exe" C:\Users\me\AppData\Roaming\npm\node_modules\@aragon-agent\cli\dist\launcher.js --resume a4k2m9
1875
+ ```
1876
+
1877
+ The `PowerShell:` line is byte-for-byte the script the new window receives, so the
1878
+ fallback can never drift from what the feature actually runs.
1879
+
1851
1880
  Two environment variables exist because neither belongs in a config file:
1852
1881
 
1853
1882
  | Variable | Effect |
@@ -65,7 +65,10 @@ export interface TerminalPlanInput {
65
65
  /** New window title, ASCII, already truncated to the cell budget. */
66
66
  title: string;
67
67
  cwd: string;
68
- /** `joinCommandLine(command)` - the pasteable fallback shown to the user. */
68
+ /**
69
+ * `renderManualCommand(command, platform)`'s product: two lines on win32
70
+ * (`PowerShell:` / `cmd.exe :`), one line everywhere else.
71
+ */
69
72
  manual: string;
70
73
  }
71
74
  /** The env var that overrides the shell the new window runs (D-7). */
@@ -80,7 +83,22 @@ export declare const ARAGON_BRANCH_SHELL_ENV = "ARAGON_BRANCH_SHELL";
80
83
  * so there is exactly one place that number lives.
81
84
  */
82
85
  export declare function planTerminalLaunch(input: TerminalPlanInput): TerminalPlanResult;
83
- /** PowerShell: the whole command as one base64(UTF-16LE) blob (D-20). */
86
+ /**
87
+ * The PowerShell statement that runs the aragon command (D-1).
88
+ *
89
+ * THE LEADING `&` IS NOT DECORATION. In command position PowerShell reads a quoted
90
+ * string as an EXPRESSION, so `'C:\Program Files\nodejs\node.exe' <script>` is a
91
+ * parse error - "unexpected token ..." at column 36 - and the child is never
92
+ * created, while the parent still believes the window opened (K-1: `wt`/`start`
93
+ * forward and exit). That is exactly the report this branch exists to fix.
94
+ * `&` is the call operator: it takes the following word as a command name whatever
95
+ * its shape, and it does not change name resolution (`& 'npm.cmd'` still finds
96
+ * `npm` through PATH/PATHEXT, appendix B-8).
97
+ *
98
+ * `posixScript` MUST NOT be given the same treatment: in sh `&` means run in the
99
+ * background, so the window would reach the trailing `exec <shell> -i` immediately
100
+ * and the branch would start behind a prompt that looks idle (I-3 / K-1).
101
+ */
84
102
  export declare function powerShellScript(parts: readonly string[]): string;
85
103
  /** Bare words stay bare; everything else becomes a single-quoted literal. */
86
104
  export declare function powerShellQuote(value: string): string;
@@ -50,8 +50,21 @@ const POSIX_CANDIDATES = Object.freeze([
50
50
  ]);
51
51
  /** The env var that overrides the shell the new window runs (D-7). */
52
52
  export const ARAGON_BRANCH_SHELL_ENV = 'ARAGON_BRANCH_SHELL';
53
- /** `PowerShell` and `cmd` both stop being reliable far below the 32767 OS limit. */
54
- const BARE_POWER_SHELL_WORD_RE = /^[A-Za-z0-9._\\/:+=@-]+$/;
53
+ /**
54
+ * A bare word is one PowerShell will not read as anything else.
55
+ *
56
+ * THE FIRST CHARACTER IS THE LOAD-BEARING PART (D-3). `@name` in argument position
57
+ * is the SPLAT operator: `& node.exe -p 'process.argv.length' @foo` hands node an
58
+ * EMPTY argument list, exits 0 and prints nothing (measured on Windows PowerShell
59
+ * 5.1, branch-terminal-powershell-fix appendix B-11). As the only word on the line
60
+ * it is not even parseable - `error col=1 id=SplattingNotPermitted`. Nothing after
61
+ * the first character can do that, which is why the class differs in the first
62
+ * position. `-` stays allowed there so that every flag this feature emits
63
+ * (`--resume`) keeps its bare form, and quoting it would change nothing except the
64
+ * look of the fallback line (appendix B-6 A/B, quoting is transparent to a native
65
+ * exe).
66
+ */
67
+ const BARE_POWER_SHELL_WORD_RE = /^[A-Za-z0-9._\\/:+=\-][A-Za-z0-9._\\/:+=@\-]*$/;
55
68
  /**
56
69
  * Build the ladder for this platform, most-preferred rung first.
57
70
  *
@@ -201,9 +214,29 @@ function resolveCandidate(candidate, env) {
201
214
  function posixWindowScript(cwd, shell, command) {
202
215
  return `cd ${posixQuote(cwd)} && ${posixScript(command)} ; exec ${posixQuote(shell.path)} -i`;
203
216
  }
204
- /** PowerShell: the whole command as one base64(UTF-16LE) blob (D-20). */
217
+ /**
218
+ * The PowerShell statement that runs the aragon command (D-1).
219
+ *
220
+ * THE LEADING `&` IS NOT DECORATION. In command position PowerShell reads a quoted
221
+ * string as an EXPRESSION, so `'C:\Program Files\nodejs\node.exe' <script>` is a
222
+ * parse error - "unexpected token ..." at column 36 - and the child is never
223
+ * created, while the parent still believes the window opened (K-1: `wt`/`start`
224
+ * forward and exit). That is exactly the report this branch exists to fix.
225
+ * `&` is the call operator: it takes the following word as a command name whatever
226
+ * its shape, and it does not change name resolution (`& 'npm.cmd'` still finds
227
+ * `npm` through PATH/PATHEXT, appendix B-8).
228
+ *
229
+ * `posixScript` MUST NOT be given the same treatment: in sh `&` means run in the
230
+ * background, so the window would reach the trailing `exec <shell> -i` immediately
231
+ * and the branch would start behind a prompt that looks idle (I-3 / K-1).
232
+ */
205
233
  export function powerShellScript(parts) {
206
- return parts.map(powerShellQuote).join(' ');
234
+ // TOTAL, NOT THROWING (D-4): an empty argv cannot arise from
235
+ // `resolveAragonCommand` (T-3 pins that), and an empty script is a parse-valid
236
+ // no-op rather than a throw inside a key handler.
237
+ if (parts.length === 0)
238
+ return '';
239
+ return ['&', ...parts.map(powerShellQuote)].join(' ');
207
240
  }
208
241
  /** Bare words stay bare; everything else becomes a single-quoted literal. */
209
242
  export function powerShellQuote(value) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
5
5
  *
6
- * THIS MODULE HAS NO IO ON PURPOSE. It answers two questions and nothing else:
6
+ * THIS MODULE PRODUCES NO IO ITSELF. It answers two questions and nothing else:
7
7
  * "which argv pieces make up the aragon command" and "how does that sentence read
8
8
  * when printed to a human". Spawning lives in `session/branch-launch.ts`, and the
9
9
  * terminal ladder lives in `proc/terminal-plan.ts`, so the one part of this
@@ -12,6 +12,14 @@
12
12
  * `proc/readiness.ts` and `proc/classify.ts` are pure, `proc/supervisor.ts` is the
13
13
  * part that touches processes.
14
14
  *
15
+ * IT DOES IMPORT `proc/terminal-plan.ts` (D-5), and that is still the right
16
+ * direction: `session -> proc`. The manual line needs both quoting dialects, and
17
+ * the dialect owner is `proc/terminal-plan.ts`; putting it in `proc/` would mean
18
+ * `proc/` importing `session/`, which is the cycle the header above forbids.
19
+ * What that import brings with it is one `statSync` probe used by the PATH lookup -
20
+ * not a process, not a write - so "no IO on purpose" still describes this module's
21
+ * own behaviour.
22
+ *
15
23
  * NAMED `branch-invocation`, NOT `branch-command` (R-22): `commands/branch-command.ts`
16
24
  * is the command body, and two files with one basename make every import, stack
17
25
  * trace and grep ambiguous. What this module exports is an *invocation*.
@@ -28,7 +36,15 @@ export interface AragonCommandResolution {
28
36
  export interface BranchInvocation {
29
37
  /** `[...aragon.parts, '--resume', id]`. */
30
38
  parts: string[];
31
- /** One line, pasteable into cmd / PowerShell / sh (quoted where needed). */
39
+ /**
40
+ * One line, for the toast and the log.
41
+ *
42
+ * NOT THE PASTEABLE FORM ON WIN32 - and the defect that produced this comment is
43
+ * the proof: a double-quoted command word is a string EXPRESSION to PowerShell, so
44
+ * pasting this line into the shell Windows Terminal opens by default yields the
45
+ * same `ParserError` the new window used to. `renderManualCommand(parts,
46
+ * platform)` is the paste-ready form; on win32 it is two labelled lines.
47
+ */
32
48
  command: string;
33
49
  }
34
50
  export declare const BRANCH_RESUME_FLAG = "--resume";
@@ -54,21 +70,49 @@ export declare function resolveAragonCommand(input: {
54
70
  execPath: string;
55
71
  env: Record<string, string | undefined>;
56
72
  }): AragonCommandResolution;
57
- /** `parts` + `--resume <id>`, plus the one-line form for toasts and manual fallback. */
73
+ /**
74
+ * `parts` + `--resume <id>`, plus the ONE-LINE form for the toast and the log.
75
+ *
76
+ * The paste-ready form is NOT this field (see `BranchInvocation.command` and
77
+ * `renderManualCommand`): "one line for every shell" was the claim this branch
78
+ * disproved, and leaving it here would keep the next reader believing it.
79
+ */
58
80
  export declare function buildResumeInvocation(input: {
59
81
  id: string;
60
82
  aragon: AragonCommandResolution;
61
83
  }): BranchInvocation;
62
84
  /**
63
- * Join argv pieces into one line a human can paste into any of the three shells.
85
+ * Join argv pieces into one line a human can read at a glance.
64
86
  *
65
- * NOT A SHELL ESCAPER. This text goes to the toast, to the notice and to the
66
- * README, never back through a parser: the three platforms agree on what double
67
- * quotes mean around a path with spaces, and that is the whole requirement. The
68
- * real command lines are built by `proc/terminal-plan.ts`, which quotes for the
69
- * exact parser each rung lands in.
87
+ * NOT A SHELL ESCAPER, and no longer claimed to be a universal paste line: it is
88
+ * the RIGHT line for `cmd.exe` and for POSIX shells, and the WRONG one for
89
+ * PowerShell on win32 (a double-quoted command word is an expression there). Its
90
+ * product is what a POSIX/darwin user pastes, and what the `cmd.exe` half of the
91
+ * win32 manual block shows; the PowerShell half comes from `powerShellScript` via
92
+ * `renderManualCommand`. The real command lines are built by
93
+ * `proc/terminal-plan.ts`, which quotes for the exact parser each rung lands in.
70
94
  */
71
95
  export declare function joinCommandLine(parts: readonly string[]): string;
96
+ /**
97
+ * The paste-ready form of the command, per platform (D-5 / D-6 / I-4).
98
+ *
99
+ * ONE LINE CANNOT BE CORRECT FOR BOTH WINDOWS PARSERS. A double-quoted command word
100
+ * is a string EXPRESSION to PowerShell (the defect this branch fixes) while it is
101
+ * exactly right for `cmd.exe`; `& 'x'` is right for PowerShell and wrong for
102
+ * `cmd.exe` (which keeps the single quotes and tries to run `'x'`). So Windows gets
103
+ * two labelled lines and the user copies the one matching the terminal they opened;
104
+ * POSIX and macOS keep the single line they have always printed.
105
+ *
106
+ * The first line is `powerShellScript(parts)` VERBATIM, which is what makes I-1
107
+ * ("one command, one artefact") testable: the fallback the user pastes is the same
108
+ * text the new window receives in its `-EncodedCommand` payload.
109
+ *
110
+ * @param parts argv pieces of the aragon invocation, `--resume <id>` included.
111
+ * @param platform the platform whose parsers the two lines have to satisfy.
112
+ * @returns one line off Windows; two ASCII lines labelled `PowerShell:` and
113
+ * `cmd.exe :` on win32. Both labels are 11 columns wide on purpose.
114
+ */
115
+ export declare function renderManualCommand(parts: readonly string[], platform: NodeJS.Platform): string;
72
116
  /**
73
117
  * The second gate on D-9: an id is about to be concatenated into a command line.
74
118
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
5
5
  *
6
- * THIS MODULE HAS NO IO ON PURPOSE. It answers two questions and nothing else:
6
+ * THIS MODULE PRODUCES NO IO ITSELF. It answers two questions and nothing else:
7
7
  * "which argv pieces make up the aragon command" and "how does that sentence read
8
8
  * when printed to a human". Spawning lives in `session/branch-launch.ts`, and the
9
9
  * terminal ladder lives in `proc/terminal-plan.ts`, so the one part of this
@@ -12,11 +12,20 @@
12
12
  * `proc/readiness.ts` and `proc/classify.ts` are pure, `proc/supervisor.ts` is the
13
13
  * part that touches processes.
14
14
  *
15
+ * IT DOES IMPORT `proc/terminal-plan.ts` (D-5), and that is still the right
16
+ * direction: `session -> proc`. The manual line needs both quoting dialects, and
17
+ * the dialect owner is `proc/terminal-plan.ts`; putting it in `proc/` would mean
18
+ * `proc/` importing `session/`, which is the cycle the header above forbids.
19
+ * What that import brings with it is one `statSync` probe used by the PATH lookup -
20
+ * not a process, not a write - so "no IO on purpose" still describes this module's
21
+ * own behaviour.
22
+ *
15
23
  * NAMED `branch-invocation`, NOT `branch-command` (R-22): `commands/branch-command.ts`
16
24
  * is the command body, and two files with one basename make every import, stack
17
25
  * trace and grep ambiguous. What this module exports is an *invocation*.
18
26
  */
19
27
  import { isAbsolute } from 'node:path';
28
+ import { powerShellScript } from '../proc/terminal-plan.js';
20
29
  import { isValidSessionId } from './store.js';
21
30
  export const BRANCH_RESUME_FLAG = '--resume';
22
31
  /** The env var that overrides "what executable the new window runs" (D-7 sibling). */
@@ -65,23 +74,58 @@ export function resolveAragonCommand(input) {
65
74
  }
66
75
  return { parts: ['aragon'], kind: 'bin', origin: 'aragon on PATH' };
67
76
  }
68
- /** `parts` + `--resume <id>`, plus the one-line form for toasts and manual fallback. */
77
+ /**
78
+ * `parts` + `--resume <id>`, plus the ONE-LINE form for the toast and the log.
79
+ *
80
+ * The paste-ready form is NOT this field (see `BranchInvocation.command` and
81
+ * `renderManualCommand`): "one line for every shell" was the claim this branch
82
+ * disproved, and leaving it here would keep the next reader believing it.
83
+ */
69
84
  export function buildResumeInvocation(input) {
70
85
  const parts = [...input.aragon.parts, BRANCH_RESUME_FLAG, input.id];
71
86
  return { parts, command: joinCommandLine(parts) };
72
87
  }
73
88
  /**
74
- * Join argv pieces into one line a human can paste into any of the three shells.
89
+ * Join argv pieces into one line a human can read at a glance.
75
90
  *
76
- * NOT A SHELL ESCAPER. This text goes to the toast, to the notice and to the
77
- * README, never back through a parser: the three platforms agree on what double
78
- * quotes mean around a path with spaces, and that is the whole requirement. The
79
- * real command lines are built by `proc/terminal-plan.ts`, which quotes for the
80
- * exact parser each rung lands in.
91
+ * NOT A SHELL ESCAPER, and no longer claimed to be a universal paste line: it is
92
+ * the RIGHT line for `cmd.exe` and for POSIX shells, and the WRONG one for
93
+ * PowerShell on win32 (a double-quoted command word is an expression there). Its
94
+ * product is what a POSIX/darwin user pastes, and what the `cmd.exe` half of the
95
+ * win32 manual block shows; the PowerShell half comes from `powerShellScript` via
96
+ * `renderManualCommand`. The real command lines are built by
97
+ * `proc/terminal-plan.ts`, which quotes for the exact parser each rung lands in.
81
98
  */
82
99
  export function joinCommandLine(parts) {
83
100
  return parts.map(quoteForDisplay).join(' ');
84
101
  }
102
+ /**
103
+ * The paste-ready form of the command, per platform (D-5 / D-6 / I-4).
104
+ *
105
+ * ONE LINE CANNOT BE CORRECT FOR BOTH WINDOWS PARSERS. A double-quoted command word
106
+ * is a string EXPRESSION to PowerShell (the defect this branch fixes) while it is
107
+ * exactly right for `cmd.exe`; `& 'x'` is right for PowerShell and wrong for
108
+ * `cmd.exe` (which keeps the single quotes and tries to run `'x'`). So Windows gets
109
+ * two labelled lines and the user copies the one matching the terminal they opened;
110
+ * POSIX and macOS keep the single line they have always printed.
111
+ *
112
+ * The first line is `powerShellScript(parts)` VERBATIM, which is what makes I-1
113
+ * ("one command, one artefact") testable: the fallback the user pastes is the same
114
+ * text the new window receives in its `-EncodedCommand` payload.
115
+ *
116
+ * @param parts argv pieces of the aragon invocation, `--resume <id>` included.
117
+ * @param platform the platform whose parsers the two lines have to satisfy.
118
+ * @returns one line off Windows; two ASCII lines labelled `PowerShell:` and
119
+ * `cmd.exe :` on win32. Both labels are 11 columns wide on purpose.
120
+ */
121
+ export function renderManualCommand(parts, platform) {
122
+ if (platform !== 'win32')
123
+ return joinCommandLine(parts);
124
+ return [
125
+ `PowerShell: ${powerShellScript(parts)}`,
126
+ `cmd.exe : ${joinCommandLine(parts)}`,
127
+ ].join('\n');
128
+ }
85
129
  /** Empty and whitespace-or-quote-bearing pieces get double quotes; the rest stay bare. */
86
130
  function quoteForDisplay(part) {
87
131
  if (part.length === 0)
@@ -29,6 +29,15 @@ export interface BranchRequest {
29
29
  }
30
30
  export type BranchOpenResult = {
31
31
  ok: true;
32
+ /**
33
+ * The paste-ready command block for this platform (D-7).
34
+ *
35
+ * "Always present, so no failure is a dead end" is I-4 for the FAILURE arm;
36
+ * the success arm carries it for the mirror-image reason - "what did the
37
+ * window I just opened actually run" is copyable without retyping it from the
38
+ * toast. Same meaning as the field of the same name below: on win32 it is the
39
+ * two-line PowerShell / `cmd.exe` block, off win32 it is one line.
40
+ */
32
41
  command: string;
33
42
  shell: string;
34
43
  rung: string;
@@ -22,7 +22,7 @@ import { spawn } from 'node:child_process';
22
22
  import { getLogger } from '../logging/logger.js';
23
23
  import { defaultIsExecutable, pathDirsOf, planTerminalLaunch, resolveShell, truncateTitle, } from '../proc/terminal-plan.js';
24
24
  import { BRANCH_COPY } from '../commands/branch-command.js';
25
- import { buildResumeInvocation, joinCommandLine, resolveAragonCommand, validateBranchId, } from './branch-invocation.js';
25
+ import { buildResumeInvocation, renderManualCommand, resolveAragonCommand, validateBranchId, } from './branch-invocation.js';
26
26
  import { BRANCH_TERMINAL_LIMITS } from './limits.js';
27
27
  /**
28
28
  * Build the port. The only mutable state is this closure's four values, none of
@@ -41,12 +41,16 @@ export function createBranchLauncher(deps) {
41
41
  let cachedAragon = null;
42
42
  let cachedShell = null;
43
43
  /**
44
- * Resolve WHAT the new window runs, and the command line built from it.
44
+ * Resolve WHAT the new window runs, and the two command forms built from it.
45
45
  *
46
46
  * The resolution is cached for the process (R-13 / K-14). It costs no
47
47
  * filesystem probe - the three rules are env, `argv[1]` and a literal - so the
48
48
  * cache is about identity, not speed: the value that opened the first window is
49
49
  * the value the log keeps reporting.
50
+ *
51
+ * `manual` is derived HERE from the same `parts` on purpose (D-7): one source of
52
+ * truth for the command means the paste block and the `-EncodedCommand` payload
53
+ * cannot drift apart (I-1).
50
54
  */
51
55
  function planFor(id) {
52
56
  cachedAragon ??= resolveAragonCommand({
@@ -54,9 +58,11 @@ export function createBranchLauncher(deps) {
54
58
  execPath: deps.execPath,
55
59
  env: deps.env,
56
60
  });
61
+ const invocation = buildResumeInvocation({ id, aragon: cachedAragon });
57
62
  return {
58
63
  aragon: cachedAragon,
59
- invocation: buildResumeInvocation({ id, aragon: cachedAragon }),
64
+ invocation,
65
+ manual: renderManualCommand(invocation.parts, deps.platform),
60
66
  };
61
67
  }
62
68
  function shellFor() {
@@ -93,22 +99,25 @@ export function createBranchLauncher(deps) {
93
99
  return cooldown(command, Math.max(1, windowMs - waited));
94
100
  }
95
101
  async function openOnce(request) {
96
- const { aragon, invocation } = planFor(request.id);
102
+ const { aragon, invocation, manual } = planFor(request.id);
97
103
  const command = invocation.command;
98
104
  // 4.5 BEFORE ANY SPAWN (R-7 / T-46): a command line that `cmd` or AppleScript
99
105
  // would truncate is worse than no command line, because the window opens and
100
- // then fails in a way this process cannot see.
106
+ // then fails in a way this process cannot see. The MEASURED object stays the
107
+ // single unquoted line: it is the lower bound of every dialect (quoting only
108
+ // ever adds characters), so the check is conservative and needs no second
109
+ // source of truth.
101
110
  if (command.length > BRANCH_TERMINAL_LIMITS.maxCommandLineCells) {
102
111
  // A COMPLETE SENTENCE WITH THE FALLBACK INSIDE IT (I-4 / appendix A): the
103
112
  // over-long case is the one failure where the user most needs a command to
104
113
  // paste, so it may not report the bare reason "command line too long" - that
105
114
  // is an internal code, not an answer, and it would leave a dead end.
106
- return failure('spawn-failed', BRANCH_COPY.spawnFailed('command line too long', command), command);
115
+ return failure('spawn-failed', BRANCH_COPY.spawnFailed('command line too long', manual), manual);
107
116
  }
108
117
  // D-9's second gate: an id is about to enter a command line.
109
118
  const invalid = validateBranchId(request.id);
110
119
  if (invalid !== null)
111
- return failure('spawn-failed', invalid, command);
120
+ return failure('spawn-failed', invalid, manual);
112
121
  // Step 6: flush FIRST, spawn only on `ok` (I-1). `force` skips the fingerprint
113
122
  // short circuit and nothing else (D-15 / I-7).
114
123
  const flushed = deps.checkpoint({ force: true });
@@ -118,7 +127,10 @@ export function createBranchLauncher(deps) {
118
127
  // disk problem, so it must not be printed as "could not checkpoint
119
128
  // (no-identity)" - that reads like corruption. It gets `/session`'s sentence.
120
129
  const message = reason === 'no-identity' ? BRANCH_COPY.noIdentity : BRANCH_COPY.checkpointFailed(reason);
121
- return failure('checkpoint-failed', message, command);
130
+ // The THIRD argument changes, never the sentence (parent-R-19): these two
131
+ // failures deliberately do NOT embed the command, because their job is to
132
+ // explain why nothing opened. The command still travels in `result.command`.
133
+ return failure('checkpoint-failed', message, manual);
122
134
  }
123
135
  const shell = shellFor();
124
136
  // Step 7: the TITLE is what gets truncated, never the id in the command line (T-47).
@@ -132,11 +144,13 @@ export function createBranchLauncher(deps) {
132
144
  command: invocation.parts,
133
145
  title,
134
146
  cwd: request.cwd,
135
- manual: joinCommandLine(invocation.parts),
147
+ // The paste block the fallback prints, built by the same dialect owner that
148
+ // encodes the window script (I-1: one command, one artefact).
149
+ manual,
136
150
  });
137
151
  if (!planned.ok) {
138
152
  // Not cached: the next press must be allowed to find a terminal that appeared.
139
- return failure('no-launcher', BRANCH_COPY.noLauncher(planned.manual), command);
153
+ return failure('no-launcher', BRANCH_COPY.noLauncher(planned.manual), manual);
140
154
  }
141
155
  // Step 8: walk the ladder, bounded by maxRungs (T-33).
142
156
  const rungs = planned.plans.slice(0, BRANCH_TERMINAL_LIMITS.maxRungs);
@@ -156,14 +170,14 @@ export function createBranchLauncher(deps) {
156
170
  aragon: aragon.kind,
157
171
  origin: aragon.origin,
158
172
  });
159
- return { ok: true, command, shell: planned.shell.path, rung: plan.rung };
173
+ return { ok: true, command: manual, shell: planned.shell.path, rung: plan.rung };
160
174
  }
161
175
  logger.warn('rung_failed', { rung: plan.rung, message: outcome.message });
162
176
  lastError = outcome;
163
177
  }
164
178
  // Step 9: every rung failed. `wt`'s own semantic failures land here too only if
165
179
  // the error event fired; a forwarded-but-rejected `wt` invocation does not (K-1).
166
- return failure('spawn-failed', BRANCH_COPY.spawnFailed(lastError?.message ?? 'no rung available', command), command);
180
+ return failure('spawn-failed', BRANCH_COPY.spawnFailed(lastError?.message ?? 'no rung available', manual), manual);
167
181
  }
168
182
  /**
169
183
  * One rung, with EXACTLY ONE `'error'` LISTENER THAT IS NEVER REMOVED (R-3 / I-10).
@@ -230,7 +244,10 @@ export function createBranchLauncher(deps) {
230
244
  }
231
245
  return {
232
246
  async open(request) {
233
- const blocked = gated(planFor(request.id).invocation.command);
247
+ // The cooldown path carries the same paste block. It never displays it, but the
248
+ // result type has exactly one meaning for `command` and inventing a second one
249
+ // for this arm is how the two forms would drift apart (D-7).
250
+ const blocked = gated(planFor(request.id).manual);
234
251
  if (blocked !== null)
235
252
  return blocked;
236
253
  inFlight = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.38",
59
+ "@aragon-agent/core": "^0.2.39",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",