@aragon-agent/cli 0.6.24 → 0.6.26

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +1924 -1786
  2. package/README.md +2730 -2562
  3. package/dist/agent/controller.d.ts +49 -3
  4. package/dist/agent/controller.js +78 -3
  5. package/dist/agent/conversation-reset.d.ts +91 -0
  6. package/dist/agent/conversation-reset.js +66 -0
  7. package/dist/agent/model-probe-limits.d.ts +39 -0
  8. package/dist/agent/model-probe-limits.js +25 -0
  9. package/dist/agent/model-probe.d.ts +104 -0
  10. package/dist/agent/model-probe.js +391 -0
  11. package/dist/agent/reducer.d.ts +22 -0
  12. package/dist/agent/reducer.js +49 -14
  13. package/dist/cli.js +214 -2
  14. package/dist/commands/branch-command.d.ts +61 -0
  15. package/dist/commands/branch-command.js +117 -0
  16. package/dist/commands/builtins.js +62 -40
  17. package/dist/commands/clear-command.d.ts +27 -0
  18. package/dist/commands/clear-command.js +107 -0
  19. package/dist/commands/registry.d.ts +48 -1
  20. package/dist/commands/session-command.d.ts +54 -0
  21. package/dist/commands/session-command.js +109 -0
  22. package/dist/config/load.d.ts +15 -0
  23. package/dist/config/model-profiles.d.ts +8 -0
  24. package/dist/config/model-profiles.js +10 -0
  25. package/dist/exec/index.js +10 -1
  26. package/dist/logging/logger.d.ts +1 -1
  27. package/dist/proc/terminal-plan.d.ts +155 -0
  28. package/dist/proc/terminal-plan.js +360 -0
  29. package/dist/session/branch-invocation.d.ts +83 -0
  30. package/dist/session/branch-invocation.js +109 -0
  31. package/dist/session/branch-launch.d.ts +70 -0
  32. package/dist/session/branch-launch.js +248 -0
  33. package/dist/session/checkpoint.d.ts +257 -0
  34. package/dist/session/checkpoint.js +512 -0
  35. package/dist/session/clear-archive.d.ts +51 -0
  36. package/dist/session/clear-archive.js +160 -0
  37. package/dist/session/cli-commands.d.ts +2 -0
  38. package/dist/session/cli-commands.js +36 -5
  39. package/dist/session/identity.d.ts +72 -0
  40. package/dist/session/identity.js +148 -0
  41. package/dist/session/limits.d.ts +66 -0
  42. package/dist/session/limits.js +66 -0
  43. package/dist/session/persist.d.ts +30 -5
  44. package/dist/session/startup-session.d.ts +107 -0
  45. package/dist/session/startup-session.js +315 -0
  46. package/dist/session/store.d.ts +60 -1
  47. package/dist/session/store.js +95 -6
  48. package/dist/todo/types.d.ts +5 -3
  49. package/dist/ui/App.d.ts +54 -2
  50. package/dist/ui/App.js +154 -28
  51. package/dist/ui/Header.js +8 -16
  52. package/dist/ui/StatusBar.d.ts +8 -0
  53. package/dist/ui/StatusBar.js +12 -3
  54. package/dist/ui/entries/CompactionCard.d.ts +5 -4
  55. package/dist/ui/entries/CompactionCard.js +5 -4
  56. package/dist/ui/layout/header-hints.d.ts +68 -0
  57. package/dist/ui/layout/header-hints.js +138 -0
  58. package/dist/ui/layout/session-chip.d.ts +52 -0
  59. package/dist/ui/layout/session-chip.js +64 -0
  60. package/dist/ui/layout/status-layout.d.ts +10 -0
  61. package/dist/ui/layout/status-layout.js +8 -1
  62. package/dist/ui/overlays/HelpOverlay.js +17 -5
  63. package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
  64. package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
  65. package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
  66. package/dist/ui/overlays/ModelProfileEditor.js +117 -68
  67. package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
  68. package/dist/ui/overlays/SettingsScreen.js +34 -2
  69. package/dist/ui/overlays/model-probe-view.d.ts +128 -0
  70. package/dist/ui/overlays/model-probe-view.js +225 -0
  71. package/dist/ui/overlays/model-profile-state.d.ts +9 -1
  72. package/dist/ui/overlays/model-profile-state.js +10 -0
  73. package/dist/ui/overlays/use-model-probe.d.ts +30 -0
  74. package/dist/ui/overlays/use-model-probe.js +109 -0
  75. package/dist/ui/session-bridge.d.ts +26 -0
  76. package/dist/ui/session-bridge.js +32 -0
  77. package/package.json +2 -2
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `/clear` - start a new conversation - plus its `/reset` alias and the
3
+ * `/clear screen` form that keeps the old "just wipe the screen" meaning
4
+ * (clear-command-context-reset §3.5 / D-1 / D-2 / D-3).
5
+ *
6
+ * ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`. The one
7
+ * localized string below is written with `\u` escapes for that reason, exactly
8
+ * as the `/reset` body it replaces did.
9
+ *
10
+ * WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
11
+ * reason `/compact` and `/context` do (D-8). `builtins.ts` is 1511 lines against
12
+ * `CLAUDE.md`'s 1000-line cap, and this command is no longer a two-line
13
+ * projection reset - it interrupts a run, waits for it, archives a conversation
14
+ * and then clears. A two-line edit belongs inline; this does not.
15
+ *
16
+ * THE COMMAND'S ONLY JOB IS SEQUENCING AND SPEAKING. Every state change happens
17
+ * on the controller port (`AgentController.resetConversation`, which delegates
18
+ * the ORDER to `agent/conversation-reset.ts`), and every word the user reads is
19
+ * decided here, in one place, in the exact order they will read it.
20
+ */
21
+ import { getLogger } from '../logging/logger.js';
22
+ /** The toast a successful clear shows. Pinned verbatim by an existing test. */
23
+ const CLEARED_TOAST = 'Started a new conversation.';
24
+ /**
25
+ * Run `/clear` or `/reset`. Both registry entries point HERE (D-2), which is
26
+ * what stops the two from drifting back into "one clears the screen, one clears
27
+ * the session" - the drift that produced the defect this feature fixes.
28
+ */
29
+ export async function runClearCommand(ctx) {
30
+ const verb = ctx.args.trim().toLowerCase();
31
+ if (verb === 'screen') {
32
+ // The old behaviour, byte for byte (I-3): silent, transcript plus the todo
33
+ // panel, history untouched.
34
+ clearScreenOnly(ctx);
35
+ return;
36
+ }
37
+ if (verb !== '') {
38
+ // Rejected rather than ignored. A command that silently accepts and
39
+ // discards an argument is a surface that lies about what it understood.
40
+ ctx.notify('warn', `Unknown argument "${ctx.args.trim()}" - use /clear [screen].`);
41
+ return;
42
+ }
43
+ // READ BEFORE CLEARING (P1-2 / I-9). `clearAllQueues()` empties
44
+ // `pendingUserSteering`, so a count taken afterwards is always zero - and a
45
+ // notice that says "0 messages cancelled" is worse than no notice at all.
46
+ const cancelled = ctx.state.pendingSteering.length;
47
+ const result = await ctx.controller.resetConversation({
48
+ interrupt: true,
49
+ // A THUNK, NOT A VALUE (P0-1 / §3.3). `ctx.state` is the view as of command
50
+ // entry, and the archive happens AFTER the interrupted engine unwinds - so
51
+ // the last tool cards of the abandoned run are written to the transcript in
52
+ // between. A value captured here would archive everything except the part
53
+ // the user was looking at when they typed the command.
54
+ readTranscript: () => ctx.transcriptEntries?.() ?? ctx.state.entries,
55
+ });
56
+ if (!result.ok) {
57
+ // ZERO SIDE EFFECTS (I-4), and a trace (P1-5). The user gets one sentence
58
+ // naming the next action; the log gets the reason, because "my /clear did
59
+ // nothing" is otherwise unanswerable after the fact.
60
+ getLogger().warn('agent', 'clear_still_running', { reason: result.reason });
61
+ ctx.notify('warn', result.message);
62
+ return;
63
+ }
64
+ // THE VIEW GOES LAST (B-5 / §3.3). `App`'s dispatch wrapper zeroes the run
65
+ // metrics, the interaction phase and the scroll anchor on this action, so
66
+ // dispatching it after the engine has settled is what makes "cleared" mean
67
+ // "nothing is still writing into it".
68
+ ctx.dispatch({ type: 'resetConversation' });
69
+ // THE ONE CONSUMER OF `messagesCleared` (P2-1). It cannot join the toast:
70
+ // `clear-command-todo.test.ts` pins that string verbatim, and a count nobody
71
+ // asked for is noise in the one place the user reads confirmations.
72
+ getLogger().info('agent', 'conversation_cleared', {
73
+ messagesCleared: result.messagesCleared,
74
+ interrupted: result.interrupted,
75
+ });
76
+ ctx.toast('info', CLEARED_TOAST);
77
+ // REINSTATED FROM THE OLD `/reset` (P1-1 / C-3). Dropping it would silently
78
+ // delete a user-visible acknowledgement that already existed and was already
79
+ // pinned by test.
80
+ if (cancelled) {
81
+ ctx.notify('warn', `\u5df2\u53d6\u6d88 ${cancelled} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
82
+ }
83
+ if (result.archivedPath) {
84
+ // A PATH RATHER THAN A PROMISE: the user can copy it, and `/resume` accepts
85
+ // an absolute path. "Archived" with no way to find the file is a reassurance,
86
+ // not a recovery.
87
+ ctx.notify('info', `Previous conversation archived: ${result.archivedPath} (/resume <path> to restore)`);
88
+ }
89
+ }
90
+ /**
91
+ * Today's `/clear`, unchanged and silent (D-3 / I-3).
92
+ *
93
+ * Kept as a real form rather than as a separate command because "clear the
94
+ * screen" and "clear the conversation" are two different intentions, and a user
95
+ * who wants the first must not have to risk the second. It also gives
96
+ * `clearTranscript` a live dispatcher: `CLAUDE.md` forbids orphaned actions, and
97
+ * this was the last one that would have had none.
98
+ */
99
+ function clearScreenOnly(ctx) {
100
+ // A USER OVERRIDE OF A PROJECTION, the same class as `/todo clear`, so it
101
+ // reuses reason `'user'`; the distinction against `clearMessages()`'s `'reset'`
102
+ // is recorded on `TodoStore`. Clearing the STORE and not merely the view
103
+ // mirror is what keeps `/save`, `/todo status|continue` and follow-through
104
+ // agreeing with the screen - they read the store directly.
105
+ ctx.controller.clearTodos();
106
+ ctx.dispatch({ type: 'clearTranscript' });
107
+ }
@@ -6,10 +6,12 @@ import type { SubmitMessageOptions } from '../agent/prompt-options.js';
6
6
  import type { CopyRequest, CopyRequestResult } from '../ui/clipboard-task.js';
7
7
  import type { AgentController } from '../agent/controller.js';
8
8
  import type { AgentMode } from '../agent/agent-mode.js';
9
- import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
9
+ import type { Entry, NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
10
10
  import type { PersistedConfig } from '../config/schema.js';
11
11
  import type { FollowThroughBudget } from '../todo/follow-through.js';
12
12
  import type { UpdateCommandPort } from '../update/types.js';
13
+ import type { SessionCommandPort } from './session-command.js';
14
+ import type { BranchCommandPort } from './branch-command.js';
13
15
  export interface CommandContext {
14
16
  /** App-owned shared lock and single feedback path for command and selection copies. */
15
17
  requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
@@ -108,6 +110,51 @@ export interface CommandContext {
108
110
  * stream is handed over directly.
109
111
  */
110
112
  writeForeign?: (text: string) => void;
113
+ /**
114
+ * The LIVE transcript rows, for `/clear`'s archive (clear-command-context-reset
115
+ * P0-1 / §3.5).
116
+ *
117
+ * A THUNK, AND THAT IS THE WHOLE POINT. The `state` field above is a snapshot
118
+ * of `ViewState` taken when the command started; `/clear` interrupts a run and
119
+ * then waits for the engine to unwind, so the last tool cards of that run are
120
+ * written to the transcript AFTER `state` was captured. Reading `state.entries`
121
+ * would archive everything except the part the user was just looking at.
122
+ *
123
+ * OPTIONAL, like `followBudget` / `update` / `mouse`: every existing
124
+ * `CommandContext` fixture in this package builds the object by literal, and a
125
+ * required field would break all of them at compile time for a feature none of
126
+ * them are about. Absent means the archive degrades to history-only.
127
+ *
128
+ * `Entry` arrives as a TYPE-ONLY import so this module keeps its runtime shape
129
+ * (the same rule the `update/types.js` import above follows).
130
+ */
131
+ transcriptEntries?: () => Entry[];
132
+ /**
133
+ * The session's read-only identity plus the two actions `/session` and
134
+ * `/resume` need (tui-session-id-resume 3.11 / D-14 / D-15).
135
+ *
136
+ * OPTIONAL, like `followBudget` / `update` / `mouse` / `transcriptEntries`
137
+ * above: every existing `CommandContext` fixture in this package builds this
138
+ * object by literal, and a required field would break all of them at compile
139
+ * time for a feature none of them is about. Absent means the build has no
140
+ * session identity, and `/session` says exactly that rather than pretending.
141
+ */
142
+ session?: SessionCommandPort;
143
+ /**
144
+ * The `/branch` port: open a new terminal on this session (tui-branch-terminal-
145
+ * shortcut 3.11 / #8).
146
+ *
147
+ * OPTIONAL for the same reason `session` above is: every existing
148
+ * `CommandContext` fixture in this package builds this object by literal, and a
149
+ * required field would break all of them at compile time for a feature none of
150
+ * them is about. Absent means `/branch` says "not available in this build"
151
+ * rather than failing silently.
152
+ *
153
+ * TYPE-ONLY import, following the rule the `session-command.js` import above
154
+ * records: the command needs the SHAPE of the port and of its result, never a
155
+ * value at run time.
156
+ */
157
+ branch?: BranchCommandPort;
111
158
  }
112
159
  /** What `/mouse` can see and do. Read at command time, never captured. */
113
160
  export interface MouseCommandPort {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `/session` - report this session's identity, and the two things a user does
3
+ * with it (tui-session-id-resume §3.11 / D-13 / D-14).
4
+ *
5
+ * ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
8
+ * reason `/clear` and `/compact` do. `builtins.ts` is already past the
9
+ * 1000-line guideline, and this command is a report, not a one-liner.
10
+ *
11
+ * COPYING THE RESUME COMMAND IS THE POINT, NOT A CONVENIENCE (D-14). Typing 6
12
+ * characters off a status bar is the only step of this whole feature that can
13
+ * fail for a human, and `/session copy` removes it: one keystroke, the exact
14
+ * command in the clipboard, no transcription.
15
+ */
16
+ import type { CheckpointResult } from '../session/checkpoint.js';
17
+ import type { SessionIdentity } from '../session/startup-session.js';
18
+ import type { CommandContext } from './registry.js';
19
+ /**
20
+ * What `/session` can see and do.
21
+ *
22
+ * THREE MEMBERS AND NO MORE. `identity()` is read at command time rather than
23
+ * captured, because `/resume` rotates the id under a live App (D-15) and a
24
+ * captured value would report the previous one for the rest of the session.
25
+ */
26
+ export interface SessionCommandPort {
27
+ /** The current identity, or `undefined` when this build has none. */
28
+ identity(): SessionIdentity | undefined;
29
+ /** Write once now; `pin` also marks the file as never-prunable. */
30
+ checkpoint(opts?: {
31
+ pin?: boolean;
32
+ }): CheckpointResult;
33
+ /** Adopt another conversation and move to a derived id; `undefined` if it cannot. */
34
+ rotate(loadedPath: string): SessionIdentity | undefined;
35
+ }
36
+ /**
37
+ * The one path an id maps to.
38
+ *
39
+ * THROUGH `sessionPathFor` RATHER THAN A SECOND `join` (§2.2): that function is
40
+ * the single choke point where an id becomes a file name, and a report that
41
+ * built its own path would be the first thing to disagree with the writer about
42
+ * where a session lives.
43
+ */
44
+ export declare function sessionFilePath(id: string): string;
45
+ /**
46
+ * Run `/session`, `/session copy` or `/session pin`.
47
+ *
48
+ * ONE NOTICE FOR THE WHOLE REPORT (§3.11 / R-20): six separate notices would put
49
+ * six cards in the transcript and replay them as six fragments on exit. The
50
+ * `file` line is NOT truncated even when it overflows: it is a path the user
51
+ * needs to be able to copy, and it is in the same family as `/save`'s
52
+ * `Saved session to <path>`.
53
+ */
54
+ export declare function runSessionCommand(ctx: CommandContext): void;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `/session` - report this session's identity, and the two things a user does
3
+ * with it (tui-session-id-resume §3.11 / D-13 / D-14).
4
+ *
5
+ * ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
8
+ * reason `/clear` and `/compact` do. `builtins.ts` is already past the
9
+ * 1000-line guideline, and this command is a report, not a one-liner.
10
+ *
11
+ * COPYING THE RESUME COMMAND IS THE POINT, NOT A CONVENIENCE (D-14). Typing 6
12
+ * characters off a status bar is the only step of this whole feature that can
13
+ * fail for a human, and `/session copy` removes it: one keystroke, the exact
14
+ * command in the clipboard, no transcription.
15
+ */
16
+ import { sessionPathFor } from '../session/store.js';
17
+ /**
18
+ * The one path an id maps to.
19
+ *
20
+ * THROUGH `sessionPathFor` RATHER THAN A SECOND `join` (§2.2): that function is
21
+ * the single choke point where an id becomes a file name, and a report that
22
+ * built its own path would be the first thing to disagree with the writer about
23
+ * where a session lives.
24
+ */
25
+ export function sessionFilePath(id) {
26
+ return sessionPathFor(id);
27
+ }
28
+ /**
29
+ * Run `/session`, `/session copy` or `/session pin`.
30
+ *
31
+ * ONE NOTICE FOR THE WHOLE REPORT (§3.11 / R-20): six separate notices would put
32
+ * six cards in the transcript and replay them as six fragments on exit. The
33
+ * `file` line is NOT truncated even when it overflows: it is a path the user
34
+ * needs to be able to copy, and it is in the same family as `/save`'s
35
+ * `Saved session to <path>`.
36
+ */
37
+ export function runSessionCommand(ctx) {
38
+ const port = ctx.session;
39
+ if (!port) {
40
+ // HONEST RATHER THAN SILENT: a build without the feature should say so,
41
+ // because "nothing happened" reads as a bug.
42
+ ctx.notify('warn', 'This session has no id (session recording is off for this build).');
43
+ return;
44
+ }
45
+ const verb = ctx.args.trim().toLowerCase();
46
+ const identity = port.identity();
47
+ if (!identity) {
48
+ ctx.notify('warn', 'This session has no id (session recording is off for this build).');
49
+ return;
50
+ }
51
+ if (verb === 'copy') {
52
+ copyCommand(ctx, identity);
53
+ return;
54
+ }
55
+ if (verb === 'pin') {
56
+ pinSession(ctx, port, identity);
57
+ return;
58
+ }
59
+ if (verb !== '') {
60
+ // Rejected rather than ignored: a command that silently discards an argument
61
+ // is a surface that lies about what it understood.
62
+ ctx.notify('warn', `Unknown argument "${ctx.args.trim()}" - use /session [copy|pin].`);
63
+ return;
64
+ }
65
+ report(ctx, identity);
66
+ }
67
+ /** The six-line report. Pure read: no checkpoint, no toast, no dispatch. */
68
+ function report(ctx, identity) {
69
+ const resume = `aragon --resume ${identity.id}`;
70
+ const lines = [
71
+ `id ${identity.id}`,
72
+ `file ${sessionFilePath(identity.id)}`,
73
+ identity.parentId
74
+ ? `branch ${identity.parentId} (this session does not write to it)`
75
+ : 'branch (none - this session started here)',
76
+ `pinned ${identity.pinned ? 'yes' : 'no'}`,
77
+ `resume ${resume}`,
78
+ 'save as aragon --save <new-id>',
79
+ ];
80
+ ctx.notify('info', lines.join('\n'));
81
+ }
82
+ /** `/session copy` - the exact command, through the SAME door `/copy` uses. */
83
+ function copyCommand(ctx, identity) {
84
+ const text = `aragon --resume ${identity.id}`;
85
+ if (!ctx.requestCopy) {
86
+ ctx.notify('warn', 'Clipboard coordinator unavailable.');
87
+ return;
88
+ }
89
+ void ctx.requestCopy({ text, lines: 1 }).then((result) => {
90
+ if (result.status === 'confirmed' || result.status === 'sent') {
91
+ ctx.toast('info', `Copied ${text}`);
92
+ return;
93
+ }
94
+ // A failed or refused copy must not look like a successful one: the command
95
+ // is short enough to read off the report above, so naming the failure is
96
+ // enough to recover.
97
+ ctx.notify('warn', result.status === 'busy' ? 'Clipboard is busy - try /session copy again.' :
98
+ `Copy failed: ${result.reason}`);
99
+ });
100
+ }
101
+ /** `/session pin` - write once and stop `sessions prune` from ever touching it. */
102
+ function pinSession(ctx, port, identity) {
103
+ const result = port.checkpoint({ pin: true });
104
+ if (!result.ok) {
105
+ ctx.notify('warn', `Could not pin this session: ${result.message}`);
106
+ return;
107
+ }
108
+ ctx.toast('info', `Session ${identity.id} pinned: aragon sessions prune will not delete it.`);
109
+ }
@@ -228,6 +228,21 @@ export interface CliFlags {
228
228
  compaction?: boolean;
229
229
  /** `--compaction-threshold <0..1|N%>` — session override for `threshold`. */
230
230
  compactionThreshold?: string;
231
+ /**
232
+ * `--resume <id|path>` - continue a saved session under a NEW id
233
+ * (tui-session-id-resume 4).
234
+ *
235
+ * A FLAG FIELD AND NOT A CONFIG KEY (D-19). There is no persisted "always
236
+ * resume this session", and no switch that shows an id without writing it:
237
+ * that state - an id a user can copy and cannot use - is worse than having no
238
+ * id at all.
239
+ *
240
+ * Carried through `toFlags` like every other flag; a field that reaches only
241
+ * `CliFlags` is inert with no compile error and no runtime error.
242
+ */
243
+ resumeSession?: string;
244
+ /** `--save <id>` - name THIS run's persistent session and pin it. */
245
+ saveSessionAs?: string;
231
246
  }
232
247
  /**
233
248
  * Resolve the effective config by merging all layers, then derive the runtime
@@ -34,6 +34,14 @@ export interface ProfileValidationIssue {
34
34
  export declare function emptyModelProfiles(): ModelProfilesConfig;
35
35
  /** Return independently mutable role state. */
36
36
  export declare function emptyModelProfileState(invalid?: boolean): ModelProfilesRuntime;
37
+ /**
38
+ * The endpoint rule published as a predicate (model-profile-connection-test §5 row 10).
39
+ *
40
+ * The probe's `invalid_base_url` guard has to use THE SAME ruler as validation, or a
41
+ * URL the editor accepts can still be refused by the test (and the reverse). A second
42
+ * copy of these four clauses is one edit away from disagreeing with the config layer.
43
+ */
44
+ export declare function isValidEndpointUrl(value: string | null | undefined): boolean;
37
45
  /** Validate untrusted input; diagnostics never include supplied values. */
38
46
  export declare function validateModelProfiles(value: unknown): ProfileValidationIssue[];
39
47
  /**
@@ -32,6 +32,16 @@ function validUrl(value) {
32
32
  return false;
33
33
  }
34
34
  }
35
+ /**
36
+ * The endpoint rule published as a predicate (model-profile-connection-test §5 row 10).
37
+ *
38
+ * The probe's `invalid_base_url` guard has to use THE SAME ruler as validation, or a
39
+ * URL the editor accepts can still be refused by the test (and the reverse). A second
40
+ * copy of these four clauses is one edit away from disagreeing with the config layer.
41
+ */
42
+ export function isValidEndpointUrl(value) {
43
+ return validUrl(value);
44
+ }
35
45
  function validateEntry(value, index) {
36
46
  const path = `modelProfiles.entries[${index}]`;
37
47
  const invalid = (field, message) => ({
@@ -418,7 +418,16 @@ function planSession(options, cwd, err) {
418
418
  if (options.sessionId !== undefined) {
419
419
  // THE UPSERT. A wrapper that mints its own id must not have to ask whether
420
420
  // this is the first turn; that is the whole reason this form exists.
421
- return { id: options.sessionId, saved: readSessionFile(sessionPathFor(options.sessionId)), saving, cwd };
421
+ //
422
+ // AND IT IS A RESUME FORM, so the same write-back rule applies (tui-session-
423
+ // id-resume D-20 / I-3). When a file already exists under that id,
424
+ // `resumedSessionId` decides which id this run writes - and for a TUI
425
+ // checkpoint that answer is a NEW uuid. Without this line the wrapper-friendly
426
+ // upsert was the one remaining way to reach a TUI checkpoint and replace it,
427
+ // which is silent, unrecoverable loss of the user's live conversation.
428
+ const path = sessionPathFor(options.sessionId);
429
+ const saved = readSessionFile(path);
430
+ return { id: saved ? resumedSessionId(path, saved) : options.sessionId, saved, saving, cwd };
422
431
  }
423
432
  return { id: randomUUID(), saved: null, saving, cwd };
424
433
  }
@@ -23,7 +23,7 @@
23
23
  import type { LogConfig } from '../config/schema.js';
24
24
  import { type LogLevelName } from './levels.js';
25
25
  /** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
26
- export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction';
26
+ export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction' | 'session' | 'branch';
27
27
  export interface LogRecord {
28
28
  ts: string;
29
29
  lv: LogLevelName;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * What a new terminal window looks like from the outside (tui-branch-terminal-shortcut
3
+ * 3.7).
4
+ *
5
+ * ASCII ONLY - `src/proc/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * PURE, AND THAT IS THE POINT. Three platforms, at least ten terminal emulators,
8
+ * four exec-argument conventions and Windows' three layers of parsing (we -> `cmd`
9
+ * -> `PowerShell`) all collapse into "what should we spawn, with which argv". A
10
+ * command body that called `spawn('cmd', ['/c','start', ...])` inline would bury
11
+ * the only testable part of this feature - the command line itself - inside
12
+ * untestable IO. `proc/readiness.ts` and `proc/classify.ts` set the same precedent
13
+ * inside this directory.
14
+ *
15
+ * WHY SESSION CODE MAY DEPEND ON THIS MODULE: it imports nothing from
16
+ * `node:child_process`, reads no configuration and creates no process. The single
17
+ * place a process is created stays in `session/branch-launch.ts`, so `proc/` keeps
18
+ * meaning "processes" and `session/` keeps meaning "sessions".
19
+ *
20
+ * EVERY RUNG CARRIES ITS OWN WORKING DIRECTORY (D-16 / D-23 / I-9). This is the
21
+ * one thing a reviewer will assume is free and is not: `spawn({ cwd })` decides the
22
+ * cwd of the process WE start, and for `wt -w 0 nt` (a new tab in an already
23
+ * running Windows Terminal) or `osascript` (a new Terminal.app window) that
24
+ * process merely forwards the request - the window opens in the terminal's own
25
+ * default directory. So `wt` passes `-d`, and both script rungs start with
26
+ * `cd <cwd> &&`. `--resume` does not restore a cwd, so a window that opens in the
27
+ * wrong place can never be corrected from inside the branch.
28
+ */
29
+ /** The three ladders, collapsed from `NodeJS.Platform` (win32 / darwin / everything else). */
30
+ export type TerminalPlatform = 'win32' | 'darwin' | 'posix';
31
+ export interface ShellInvocation {
32
+ /** Executable: an absolute path or a name resolved through PATH. */
33
+ path: string;
34
+ /** Which command syntax the aragon command has to be encoded for. */
35
+ kind: 'powershell' | 'posix';
36
+ }
37
+ export interface TerminalLaunchPlan {
38
+ /** The program we spawn. */
39
+ file: string;
40
+ args: string[];
41
+ /** True only for the `cmd` rung, whose command line `cmd.exe` must see verbatim. */
42
+ windowsVerbatimArguments: boolean;
43
+ /** Ladder name, for the log and for the success toast's debug detail. */
44
+ rung: string;
45
+ }
46
+ export type TerminalPlanResult = {
47
+ ok: true;
48
+ plans: TerminalLaunchPlan[];
49
+ shell: ShellInvocation;
50
+ manual: string;
51
+ } | {
52
+ ok: false;
53
+ reason: 'no-launcher';
54
+ shell: ShellInvocation;
55
+ manual: string;
56
+ };
57
+ export interface TerminalPlanInput {
58
+ platform: NodeJS.Platform;
59
+ env: Record<string, string | undefined>;
60
+ pathDirs: readonly string[];
61
+ isExecutable?: (name: string) => boolean;
62
+ shell: ShellInvocation;
63
+ /** The aragon command's argv pieces (`session/branch-invocation.ts`'s product). */
64
+ command: readonly string[];
65
+ /** New window title, ASCII, already truncated to the cell budget. */
66
+ title: string;
67
+ cwd: string;
68
+ /** `joinCommandLine(command)` - the pasteable fallback shown to the user. */
69
+ manual: string;
70
+ }
71
+ /** The env var that overrides the shell the new window runs (D-7). */
72
+ export declare const ARAGON_BRANCH_SHELL_ENV = "ARAGON_BRANCH_SHELL";
73
+ /**
74
+ * Build the ladder for this platform, most-preferred rung first.
75
+ *
76
+ * The return value is a LIST because the failures this feature can actually hit
77
+ * are synchronous argument rejections (ENOENT on `wt.exe`, no X display), and a
78
+ * list lets `branch-launch.ts` walk down it without knowing anything about
79
+ * terminals. Rung COUNT is bounded by the caller (`BRANCH_TERMINAL_LIMITS.maxRungs`),
80
+ * so there is exactly one place that number lives.
81
+ */
82
+ export declare function planTerminalLaunch(input: TerminalPlanInput): TerminalPlanResult;
83
+ /** PowerShell: the whole command as one base64(UTF-16LE) blob (D-20). */
84
+ export declare function powerShellScript(parts: readonly string[]): string;
85
+ /** Bare words stay bare; everything else becomes a single-quoted literal. */
86
+ export declare function powerShellQuote(value: string): string;
87
+ /**
88
+ * `-EncodedCommand` takes base64 of UTF-16LE, which needs no shell quoting at all.
89
+ *
90
+ * The alphabet is `A-Za-z0-9+/=` - no spaces, no quotes, no `cmd` metacharacters -
91
+ * so the blob survives all three of Windows' parsers as an ordinary word. This is
92
+ * the same trick `ui/win-vt-force.ts` uses, for the same reason: encode the fragile
93
+ * thing into a form nothing can argue with, at construction time. Our command line
94
+ * necessarily contains spaced paths (`C:\Program Files\nodejs\node.exe`), so the
95
+ * fragile thing is not hypothetical.
96
+ */
97
+ export declare function encodePowerShellCommand(script: string): string;
98
+ /** POSIX single-quote escaping: `'` becomes `'\''`. */
99
+ export declare function posixQuote(value: string): string;
100
+ export declare function posixScript(parts: readonly string[]): string;
101
+ /** AppleScript string literals only need `\` and `"` escaped. */
102
+ export declare function escapeAppleScriptText(value: string): string;
103
+ /**
104
+ * Win32 argv quoting for the `cmd` rung's title and shell path: quote when it
105
+ * contains whitespace, otherwise leave it alone.
106
+ *
107
+ * THE INPUT DOMAIN EXCLUDES `"` BY CONSTRUCTION (R-17/T-48): Windows paths cannot
108
+ * contain it, and the title is `aragon <id>` where an id is `[A-Za-z0-9._-]`. If a
109
+ * future source can contain a quote, this must switch to `cmd`'s rule (`""`) and
110
+ * not to the POSIX `\"` written here - the two are not interchangeable.
111
+ */
112
+ export declare function quoteWin32Arg(value: string): string;
113
+ /** PATH entries for `name` probing: both spellings on Windows, `:` on POSIX. */
114
+ export declare function pathDirsOf(env: Record<string, string | undefined>, platform: NodeJS.Platform): string[];
115
+ /**
116
+ * The default probe: is this a file we could execute?
117
+ *
118
+ * An absolute or separator-bearing value is stat'd directly (that is the
119
+ * `ARAGON_BRANCH_SHELL=/usr/local/bin/fish` case); a bare name is looked up in the
120
+ * PATH directories, trying `name` and - on Windows - `name + '.exe'`, because a
121
+ * user typing `pwsh` and a probe looking for `pwsh.exe` are the same request.
122
+ *
123
+ * EVERY EXCEPTION RETURNS FALSE: one failed stat means "this candidate is not
124
+ * here", not "the feature is broken", and a missing terminal must degrade to the
125
+ * manual command rather than throw out of a key handler.
126
+ */
127
+ export declare function defaultIsExecutable(platform: NodeJS.Platform, pathDirs?: readonly string[]): (name: string) => boolean;
128
+ /**
129
+ * Which shell the new window runs (D-7), first match wins.
130
+ *
131
+ * The requirement says "PowerShell / bash", and both platforms have two answers
132
+ * worth distinguishing: PowerShell 7 (`pwsh.exe`) when the user installed it,
133
+ * Windows PowerShell 5.1 otherwise; `$SHELL` when the user's login shell is not
134
+ * bash, then bash, then `/bin/sh` as the one path that exists everywhere.
135
+ * `ARAGON_BRANCH_SHELL` is the escape hatch - an ENV VALUE, not a config key
136
+ * (D-17): a "Ctrl+L opens no terminal" switch would create the one state this
137
+ * feature must never have.
138
+ */
139
+ export declare function resolveShell(input: {
140
+ platform: NodeJS.Platform;
141
+ env: Record<string, string | undefined>;
142
+ isExecutable?: (name: string) => boolean;
143
+ pathDirs: readonly string[];
144
+ }): ShellInvocation;
145
+ /**
146
+ * Fit a window title into `maxCells` columns (R-7 / T-47).
147
+ *
148
+ * THE TITLE IS THE ONLY THING THIS TOUCHES. An id can be 64 characters long, so
149
+ * `aragon <id>` exceeds the 60-cell budget by construction for the longest ids -
150
+ * and truncating the id anywhere else (the command line, `--resume`) would produce
151
+ * a branch that cannot find its own parent. Code points, not bytes: the title is
152
+ * ASCII today, and a future non-ASCII title must not be cut through a surrogate
153
+ * pair.
154
+ */
155
+ export declare function truncateTitle(title: string, maxCells: number): string;