@songsid/agend 2.2.0-beta.5 → 2.2.0-beta.7

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 (127) hide show
  1. package/dist/agent-cli-instructions.md +6 -2
  2. package/dist/agent-cli.js +9 -5
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/backend/claude-carry.d.ts +22 -0
  5. package/dist/backend/claude-carry.js +170 -0
  6. package/dist/backend/claude-carry.js.map +1 -0
  7. package/dist/backend/claude-code.d.ts +16 -0
  8. package/dist/backend/claude-code.js +92 -2
  9. package/dist/backend/claude-code.js.map +1 -1
  10. package/dist/backend/credential-profile.d.ts +7 -0
  11. package/dist/backend/credential-profile.js +7 -1
  12. package/dist/backend/credential-profile.js.map +1 -1
  13. package/dist/backend/kiro.js +5 -2
  14. package/dist/backend/kiro.js.map +1 -1
  15. package/dist/backend/muse.d.ts +67 -0
  16. package/dist/backend/muse.js +370 -44
  17. package/dist/backend/muse.js.map +1 -1
  18. package/dist/backend/types.d.ts +7 -0
  19. package/dist/backend/types.js.map +1 -1
  20. package/dist/background-work-ended.d.ts +18 -0
  21. package/dist/background-work-ended.js +30 -0
  22. package/dist/background-work-ended.js.map +1 -0
  23. package/dist/channel/adapters/discord.js +12 -1
  24. package/dist/channel/adapters/discord.js.map +1 -1
  25. package/dist/channel/adapters/telegram.d.ts +4 -5
  26. package/dist/channel/adapters/telegram.js +39 -17
  27. package/dist/channel/adapters/telegram.js.map +1 -1
  28. package/dist/channel/mcp-tools.js +3 -3
  29. package/dist/channel/mcp-tools.js.map +1 -1
  30. package/dist/channel/message-edit-errors.d.ts +5 -0
  31. package/dist/channel/message-edit-errors.js +24 -0
  32. package/dist/channel/message-edit-errors.js.map +1 -0
  33. package/dist/channel/types.d.ts +3 -0
  34. package/dist/cli.js +25 -10
  35. package/dist/cli.js.map +1 -1
  36. package/dist/cross-instance-envelope.js +2 -0
  37. package/dist/cross-instance-envelope.js.map +1 -1
  38. package/dist/daemon.d.ts +6 -0
  39. package/dist/daemon.js +165 -54
  40. package/dist/daemon.js.map +1 -1
  41. package/dist/delivery-outbox.d.ts +27 -0
  42. package/dist/delivery-outbox.js +175 -6
  43. package/dist/delivery-outbox.js.map +1 -1
  44. package/dist/fleet-context.d.ts +4 -1
  45. package/dist/fleet-manager.d.ts +110 -6
  46. package/dist/fleet-manager.js +558 -100
  47. package/dist/fleet-manager.js.map +1 -1
  48. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +44 -9
  49. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +14 -1
  50. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  51. package/dist/human-delivery-receipts.d.ts +83 -0
  52. package/dist/human-delivery-receipts.js +194 -0
  53. package/dist/human-delivery-receipts.js.map +1 -0
  54. package/dist/instructions.d.ts +1 -1
  55. package/dist/instructions.js +1 -1
  56. package/dist/instructions.js.map +1 -1
  57. package/dist/locale.js +22 -2
  58. package/dist/locale.js.map +1 -1
  59. package/dist/needs-you-hub.js +2 -0
  60. package/dist/needs-you-hub.js.map +1 -1
  61. package/dist/needs-you-live.d.ts +6 -0
  62. package/dist/needs-you-live.js +23 -3
  63. package/dist/needs-you-live.js.map +1 -1
  64. package/dist/outbound-handlers.d.ts +10 -0
  65. package/dist/outbound-handlers.js +39 -14
  66. package/dist/outbound-handlers.js.map +1 -1
  67. package/dist/outbound-schemas.d.ts +15 -13
  68. package/dist/outbound-schemas.js +5 -0
  69. package/dist/outbound-schemas.js.map +1 -1
  70. package/dist/public-link-progress.d.ts +1 -1
  71. package/dist/public-link-progress.js +8 -2
  72. package/dist/public-link-progress.js.map +1 -1
  73. package/dist/public-preview.d.ts +2 -1
  74. package/dist/public-preview.js +8 -5
  75. package/dist/public-preview.js.map +1 -1
  76. package/dist/public-web-link.d.ts +4 -1
  77. package/dist/public-web-link.js +27 -10
  78. package/dist/public-web-link.js.map +1 -1
  79. package/dist/reply-buttons-controller.d.ts +3 -0
  80. package/dist/reply-buttons-controller.js +3 -2
  81. package/dist/reply-buttons-controller.js.map +1 -1
  82. package/dist/restart-progress.d.ts +4 -0
  83. package/dist/restart-progress.js +7 -0
  84. package/dist/restart-progress.js.map +1 -1
  85. package/dist/service-installer.d.ts +31 -0
  86. package/dist/service-installer.js +41 -2
  87. package/dist/service-installer.js.map +1 -1
  88. package/dist/topic-commands.d.ts +3 -2
  89. package/dist/topic-commands.js +17 -5
  90. package/dist/topic-commands.js.map +1 -1
  91. package/dist/tunnel/cloudflared.d.ts +7 -15
  92. package/dist/tunnel/cloudflared.js +36 -1
  93. package/dist/tunnel/cloudflared.js.map +1 -1
  94. package/dist/tunnel/types.d.ts +5 -1
  95. package/dist/tunnel/types.js.map +1 -1
  96. package/dist/ui/chat-strings.js +8 -2
  97. package/dist/ui/chat-thread.js +24 -4
  98. package/dist/ui/chat-time.js +80 -0
  99. package/dist/ui/preview-panel.js +7 -1
  100. package/dist/ui/preview.js +5 -1
  101. package/dist/ui/shared/app-stream.js +21 -4
  102. package/dist/ui/shared/app.css +9 -3
  103. package/dist/ui/shared/usage-dialog.js +10 -1
  104. package/dist/update-install.d.ts +6 -2
  105. package/dist/update-install.js +8 -2
  106. package/dist/update-install.js.map +1 -1
  107. package/dist/update-marker.d.ts +11 -0
  108. package/dist/update-marker.js +12 -0
  109. package/dist/update-marker.js.map +1 -1
  110. package/dist/web-api.d.ts +5 -1
  111. package/dist/web-api.js +8 -1
  112. package/dist/web-api.js.map +1 -1
  113. package/dist/web-commands.d.ts +2 -2
  114. package/dist/web-commands.js +3 -2
  115. package/dist/web-commands.js.map +1 -1
  116. package/dist/web-reply-files.d.ts +45 -0
  117. package/dist/web-reply-files.js +223 -0
  118. package/dist/web-reply-files.js.map +1 -0
  119. package/dist/web-server-drain.d.ts +10 -0
  120. package/dist/web-server-drain.js +50 -0
  121. package/dist/web-server-drain.js.map +1 -0
  122. package/dist/web-upload.d.ts +17 -4
  123. package/dist/web-upload.js +64 -13
  124. package/dist/web-upload.js.map +1 -1
  125. package/package.json +1 -1
  126. package/templates/launchd.plist.ejs +3 -1
  127. package/templates/systemd.service.ejs +2 -1
@@ -15,6 +15,15 @@ export declare function museSessionCwd(head: string): string | null;
15
15
  * readers — never on the fleet event loop.
16
16
  */
17
17
  export declare function readFileHeadSync(filePath: string, maxChars: number): string | null;
18
+ /**
19
+ * #1717: true when the pane shows muse's terminals panel with at least one
20
+ * running task — the "local work still active" state. Anchored to the last
21
+ * separator, where the live panel sits under the input box: a transcript
22
+ * quoting an old panel above a separator never counts. The idle proof checks
23
+ * the same shape positionally via museStatusFooter; this boolean feeds the
24
+ * pause gate below.
25
+ */
26
+ export declare function museBackgroundWorkActive(pane: string): boolean;
18
27
  /**
19
28
  * The rows of muse's live input box, or null when the bottom of the pane is not
20
29
  * muse's layout (#829). The box is the LAST thing above the status bar:
@@ -50,6 +59,55 @@ export declare function museDraftShows(rows: readonly string[], width: number, t
50
59
  * glyph allowed in front of a choice.
51
60
  */
52
61
  export declare function museLoginScreenActive(pane: string): boolean;
62
+ /**
63
+ * #1716: `/quit` while a background task runs is answered with a confirmation
64
+ * instead of an exit (live 1.4.6,
65
+ * tests/fixtures/muse-1.4.6/prod-quit-with-local-work.pane.txt):
66
+ * ❯
67
+ * Local work is still active
68
+ * The following will stop when you exit:
69
+ * background task · tool.bash
70
+ * ❯ 1. Exit anyway
71
+ * 2. Stay
72
+ * Enter to confirm · Esc to cancel
73
+ * ──────────
74
+ * main · ↓ to select
75
+ * └ ◇ Wait in the background running 15s
76
+ * mock-model · high · ~/work · Launch overrides
77
+ * Unlike Claude's prompt this menu is NOT bottom-anchored — the terminals
78
+ * panel sits under it — so the match is anchored to the hint row instead:
79
+ * the header must be within 12 rows above it with both options in between.
80
+ * Enter would take the selected option ("Exit anyway"), so like #1217 this is
81
+ * recognised whole and never answered here: the stop flow cancels it.
82
+ */
83
+ /**
84
+ * #1715: with `skip_permissions: false` muse asks before running a command
85
+ * needing approval (live 1.4.6,
86
+ * tests/fixtures/muse-1.4.6/approval-escalated.pane.txt and
87
+ * approval-untrusted-staged.pane.txt):
88
+ * Would you like to run the following command?
89
+ * $ echo perm-check
90
+ * › 1. Yes, proceed (y)
91
+ * 2. No, and tell Muse Code what to do differently (esc)
92
+ * Change approval defaults in /permissions
93
+ * `--approval-mode untrusted` (new in 1.4.6) stages the command first
94
+ * (`Stage 1/1`, `Current argv: [...]`) and offers three options (allow once /
95
+ * always allow in this workspace / abort). 1.4.4 shows the same prompt with a
96
+ * `›` marker and no footer line. Enter would take the preselected allow
97
+ * option, so like #1716 this is recognised whole and never answered here.
98
+ */
99
+ export declare const MUSE_APPROVAL_PROMPT: RegExp;
100
+ /**
101
+ * #1715: which request the approval prompt is showing — its `$ command` row.
102
+ * Stable across the `Calling tools (Ns)` timer redraws, so the episode can
103
+ * confirm; a whole-pane hash never would. Derived from the SAME selected
104
+ * block as the predicate (#1715 r2): a scrollback quote's command must never
105
+ * become the current request's identity.
106
+ */
107
+ export declare function museApprovalRequestIdentity(pane: string): string | null;
108
+ export declare function museApprovalPromptActive(pane: string): boolean;
109
+ export declare const MUSE_LOCAL_WORK_QUIT: RegExp;
110
+ export declare function museLocalWorkQuitActive(pane: string): boolean;
53
111
  /**
54
112
  * Meta Muse Code — the `muse` CLI (v1.3.0 verified).
55
113
  *
@@ -169,6 +227,15 @@ export declare class MuseBackend implements CliBackend {
169
227
  getQuitCommand(): string | null;
170
228
  getQuitKey(): string;
171
229
  getQuitKeyPresses(): number;
230
+ /** #1716: `/quit` with a background task running stops at the "Local work is still active" menu. */
231
+ quitBlockedByDialog(pane: string): boolean;
232
+ /**
233
+ * #1717: the terminals panel with a `running` task — the pause flow defers
234
+ * an idle pause while this holds, so an AgEnD-initiated silent pause never
235
+ * kills the user's running background work. Explicit stop/restart still
236
+ * proceed via quitBlockedByDialog above.
237
+ */
238
+ backgroundWorkRunning(pane: string): boolean;
172
239
  getCompactCommand(): string;
173
240
  getClearCommand(): string | null;
174
241
  /**
@@ -90,6 +90,27 @@ const MUSE_STATUS_BAR = /^\s{1,4}[^\s·]+(?:\s+·\s+[^·\s][^·]*)+$/;
90
90
  const MUSE_DIALOG_CHROME = /Allow this (?:tool|command)|Approve this (?:tool|command)|^\s*[>❯]?\s*1\s+(?:Allow|Approve|Trust)\b|Use Up\/Down|Esc (?:quits|to cancel)|↓↑ to select|·\s*Enter to choose\b/i;
91
91
  /** The status bar is one row; a long cwd in a narrow pane wraps it onto a few more. */
92
92
  const MUSE_MAX_STATUS_ROWS = 4;
93
+ /**
94
+ * #1717: extended bottom-region depth, but ONLY for a terminals-panel-headed
95
+ * block (header + running tasks + bar). A long wrapped bar still fails closed
96
+ * past MUSE_MAX_STATUS_ROWS (#958): the extension below accepts a deeper
97
+ * separator only when the row right after it is the panel header.
98
+ */
99
+ const MUSE_MAX_PANEL_DEPTH = 8;
100
+ /**
101
+ * #1717: while a background task runs, muse draws a terminals panel between
102
+ * the input box's bottom separator and the status bar (live 1.4.6,
103
+ * tests/fixtures/muse-1.4.6/prod-background-work.pane.txt):
104
+ * main · ↓ to select
105
+ * └ ◆ Wait in the background running 11s
106
+ * The header ends in `↓ to select` — note the single arrow, which is NOT the
107
+ * dialog chrome's `↓↑ to select`. The timer redraws every second, which is
108
+ * exactly the periodic-redraw case. The composer above stays free, so the
109
+ * panel must not fail the idle proof — but only this shape passes: anything
110
+ * else under the separator still fails closed.
111
+ */
112
+ const MUSE_TERMINALS_HEADER = /^.+· ↓ to select\s*$/;
113
+ const MUSE_TERMINAL_TASK_ROW = /^[└├]\s+.*\brunning\b\s+(?:\d+(?:\.\d+)?[hms]\s*)+\s*$/;
93
114
  /** The working line's timer: `◇ Thinking (2s · esc to interrupt)`. */
94
115
  const MUSE_BUSY = /\(\s*(?:\d+(?:\.\d+)?[hms]\s*)+·\s*esc to interrupt\s*\)/;
95
116
  const MUSE_MAX_INPUT_ROWS = 200;
@@ -98,10 +119,30 @@ const MUSE_MAX_INPUT_ROWS = 200;
98
119
  * first row in the bar's grammar, the rest a wrapped continuation of it. A
99
120
  * picker, a dialog, a busy line or a second separator there means the box above
100
121
  * is not the live input (#829 review: a chooser under an old box).
122
+ *
123
+ * #1717: a terminals panel may sit between the separator and the bar (a
124
+ * `· ↓ to select` header plus `running` task rows). It is stripped first and
125
+ * each of its rows must match the panel grammar — a header with no task row,
126
+ * or any other shape, still fails closed.
101
127
  */
102
128
  function museStatusFooter(rows, bottomSeparator, busy) {
103
- const status = rows.slice(bottomSeparator + 1);
104
- if (status.length === 0 || !MUSE_STATUS_BAR.test(status[0]))
129
+ let status = rows.slice(bottomSeparator + 1);
130
+ if (status.length === 0)
131
+ return false;
132
+ if (MUSE_TERMINALS_HEADER.test(status[0])) {
133
+ let end = 1;
134
+ while (end < status.length && MUSE_TERMINAL_TASK_ROW.test(status[end]))
135
+ end++;
136
+ if (end === 1)
137
+ return false;
138
+ const panel = status.slice(0, end);
139
+ if (!panel.every(row => !MUSE_SEPARATOR.test(row.trim()) && !busy.test(row) && !MUSE_DIALOG_CHROME.test(row)))
140
+ return false;
141
+ status = status.slice(end);
142
+ if (status.length === 0)
143
+ return false;
144
+ }
145
+ if (!MUSE_STATUS_BAR.test(status[0]))
105
146
  return false;
106
147
  if (!status.every(row => /^\s+\S/.test(row) && !MUSE_SEPARATOR.test(row.trim()) && !busy.test(row) && !MUSE_DIALOG_CHROME.test(row)))
107
148
  return false;
@@ -109,6 +150,85 @@ function museStatusFooter(rows, bottomSeparator, busy) {
109
150
  // selection cursor under the bar is a picker, not the bar.
110
151
  return !status.slice(1).some(row => /^\s*[>❯›]?\s*\d+[.)]?\s+\S/.test(row) || /^\s*[>›]/.test(row));
111
152
  }
153
+ /**
154
+ * The structural core of the idle proof, shared with the quit-menu matcher
155
+ * (#1730 P2-1): true when the CURRENT screen — anchored to the bottom
156
+ * separator — shows the live, empty input box. A transcript quoting an older
157
+ * screen above never satisfies it.
158
+ */
159
+ function museFreeComposerLayout(rows, busyPattern) {
160
+ // The approval prompt is auto-answered rather than input-blocking, so the
161
+ // idle proof itself must refuse a frame that shows dialog chrome.
162
+ const dialogChrome = MUSE_DIALOG_CHROME;
163
+ // Everything after the bottom separator must be the status bar (#1717:
164
+ // optionally a terminals panel first).
165
+ const bottomSeparator = museBottomSeparator(rows);
166
+ if (bottomSeparator < 2)
167
+ return false;
168
+ if (!museStatusFooter(rows, bottomSeparator, busyPattern))
169
+ return false;
170
+ // Directly above it, the EMPTY live prompt. A `❯` with text is a draft
171
+ // (or, higher up, a transcript echo) and never proves idle.
172
+ if (!/^❯\s*$/.test(rows[bottomSeparator - 1]))
173
+ return false;
174
+ const topSeparator = bottomSeparator - 2;
175
+ if (!MUSE_SEPARATOR.test(rows[topSeparator]))
176
+ return false;
177
+ // No busy indicator and no dialog just above the input box. The working
178
+ // line (`◇ Thinking (2s · esc to interrupt)`) sits immediately above the
179
+ // top separator; check 8 rows up to be safe.
180
+ const above = rows.slice(Math.max(0, topSeparator - 8), topSeparator);
181
+ if (above.some(r => busyPattern.test(r) || dialogChrome.test(r)))
182
+ return false;
183
+ // Arrow-key pickers (`/model`) are refused by their component chrome
184
+ // above, not by guessing from answer text that happens to list model ids.
185
+ // A `›` selection cursor is refused too; `❯` is not a cursor here — muse
186
+ // echoes submitted messages with it.
187
+ return !above.some(r => /^\s*›\s+\S/.test(r));
188
+ }
189
+ /**
190
+ * Nearest bottom separator, with the #1717 panel extension: the plain search
191
+ * stays capped at MUSE_MAX_STATUS_ROWS (a 5-row wrapped bar still fails
192
+ * closed, #958); a deeper separator is accepted only when the row right
193
+ * after it is the terminals-panel header. Nearest-first, so a stale
194
+ * separator in scrollback can never win over the live one.
195
+ */
196
+ function museBottomSeparator(rows) {
197
+ for (let i = rows.length - 2; i >= Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS); i--) {
198
+ if (MUSE_SEPARATOR.test(rows[i]))
199
+ return i;
200
+ }
201
+ for (let i = Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS) - 1; i >= Math.max(0, rows.length - 1 - MUSE_MAX_PANEL_DEPTH); i--) {
202
+ if (MUSE_SEPARATOR.test(rows[i])) {
203
+ return MUSE_TERMINALS_HEADER.test(rows[i + 1] ?? "") ? i : -1;
204
+ }
205
+ }
206
+ return -1;
207
+ }
208
+ /**
209
+ * #1717: true when the pane shows muse's terminals panel with at least one
210
+ * running task — the "local work still active" state. Anchored to the last
211
+ * separator, where the live panel sits under the input box: a transcript
212
+ * quoting an old panel above a separator never counts. The idle proof checks
213
+ * the same shape positionally via museStatusFooter; this boolean feeds the
214
+ * pause gate below.
215
+ */
216
+ export function museBackgroundWorkActive(pane) {
217
+ const rows = pane.replace(/\r/g, "").split("\n");
218
+ while (rows.length && !rows[rows.length - 1].trim())
219
+ rows.pop();
220
+ let sep = -1;
221
+ for (let i = rows.length - 1; i >= 0; i--) {
222
+ if (MUSE_SEPARATOR.test(rows[i])) {
223
+ sep = i;
224
+ break;
225
+ }
226
+ }
227
+ if (sep < 0)
228
+ return false;
229
+ const after = rows.slice(sep + 1);
230
+ return after.length > 1 && MUSE_TERMINALS_HEADER.test(after[0]) && MUSE_TERMINAL_TASK_ROW.test(after[1]);
231
+ }
112
232
  /**
113
233
  * The rows of muse's live input box, or null when the bottom of the pane is not
114
234
  * muse's layout (#829). The box is the LAST thing above the status bar:
@@ -127,13 +247,7 @@ export function museInputBox(pane, busy = MUSE_BUSY) {
127
247
  const rows = pane.replace(/\r/g, "").split("\n");
128
248
  while (rows.length && !rows[rows.length - 1].trim())
129
249
  rows.pop();
130
- let bottom = -1;
131
- for (let i = rows.length - 2; i >= Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS); i--) {
132
- if (MUSE_SEPARATOR.test(rows[i])) {
133
- bottom = i;
134
- break;
135
- }
136
- }
250
+ const bottom = museBottomSeparator(rows);
137
251
  if (bottom < 2 || !museStatusFooter(rows, bottom, busy))
138
252
  return null;
139
253
  for (let top = bottom - 1, n = 0; top >= 1 && n < MUSE_MAX_INPUT_ROWS; top--, n++) {
@@ -234,6 +348,214 @@ const MUSE_LOGIN_MENU = {
234
348
  inputBlocked: true,
235
349
  description: "Muse login menu — waiting for a human to log in (never answered)",
236
350
  };
351
+ /**
352
+ * #1716: `/quit` while a background task runs is answered with a confirmation
353
+ * instead of an exit (live 1.4.6,
354
+ * tests/fixtures/muse-1.4.6/prod-quit-with-local-work.pane.txt):
355
+ * ❯
356
+ * Local work is still active
357
+ * The following will stop when you exit:
358
+ * background task · tool.bash
359
+ * ❯ 1. Exit anyway
360
+ * 2. Stay
361
+ * Enter to confirm · Esc to cancel
362
+ * ──────────
363
+ * main · ↓ to select
364
+ * └ ◇ Wait in the background running 15s
365
+ * mock-model · high · ~/work · Launch overrides
366
+ * Unlike Claude's prompt this menu is NOT bottom-anchored — the terminals
367
+ * panel sits under it — so the match is anchored to the hint row instead:
368
+ * the header must be within 12 rows above it with both options in between.
369
+ * Enter would take the selected option ("Exit anyway"), so like #1217 this is
370
+ * recognised whole and never answered here: the stop flow cancels it.
371
+ */
372
+ /**
373
+ * #1715: with `skip_permissions: false` muse asks before running a command
374
+ * needing approval (live 1.4.6,
375
+ * tests/fixtures/muse-1.4.6/approval-escalated.pane.txt and
376
+ * approval-untrusted-staged.pane.txt):
377
+ * Would you like to run the following command?
378
+ * $ echo perm-check
379
+ * › 1. Yes, proceed (y)
380
+ * 2. No, and tell Muse Code what to do differently (esc)
381
+ * Change approval defaults in /permissions
382
+ * `--approval-mode untrusted` (new in 1.4.6) stages the command first
383
+ * (`Stage 1/1`, `Current argv: [...]`) and offers three options (allow once /
384
+ * always allow in this workspace / abort). 1.4.4 shows the same prompt with a
385
+ * `›` marker and no footer line. Enter would take the preselected allow
386
+ * option, so like #1716 this is recognised whole and never answered here.
387
+ */
388
+ export const MUSE_APPROVAL_PROMPT = /^[ \t]*Would you like to run the following command\?[ \t]*$/m;
389
+ const MUSE_APPROVAL_STAGE = /^[ \t]*Stage \d+\/\d+[ \t]*$/;
390
+ const MUSE_APPROVAL_ARGV = /^[ \t]*Current argv: \[.*\][ \t]*$/;
391
+ const MUSE_APPROVAL_OPT = /^[ \t]*[❯›]?[ \t]*\d\.[ \t]+/;
392
+ const MUSE_APPROVAL_FOOTER = /^[ \t]*Change approval defaults in \/permissions[ \t]*$/;
393
+ /**
394
+ * #1715: which request the approval prompt is showing — its `$ command` row.
395
+ * Stable across the `Calling tools (Ns)` timer redraws, so the episode can
396
+ * confirm; a whole-pane hash never would. Derived from the SAME selected
397
+ * block as the predicate (#1715 r2): a scrollback quote's command must never
398
+ * become the current request's identity.
399
+ */
400
+ export function museApprovalRequestIdentity(pane) {
401
+ const s = museApprovalScreen(pane);
402
+ if (s === null)
403
+ return null;
404
+ return museApprovalBlock(s.screen, s.top, s.bottom)?.command ?? null;
405
+ }
406
+ /**
407
+ * #1715 r2: the selected live approval block — its header row index and `$`
408
+ * command text. Headers are tried newest-first inside the live input block so
409
+ * a scrollback quote never shadows the current prompt, and the identity comes
410
+ * from the SAME block the predicate accepts (never a separate global search).
411
+ */
412
+ function museApprovalBlock(screen, top, bottom) {
413
+ const window = [];
414
+ for (let i = bottom - 1; i > top; i--) {
415
+ if (screen[i].trim())
416
+ window.unshift({ row: screen[i], index: i });
417
+ }
418
+ for (let h = window.length - 1; h >= 0; h--) {
419
+ if (!MUSE_APPROVAL_PROMPT.test(window[h].row))
420
+ continue;
421
+ let k = h + 1;
422
+ const commandRow = k < window.length ? window[k].row : "";
423
+ const command = /^[ \t]*\$ (.+?)[ \t]*$/.exec(commandRow)?.[1]?.trim();
424
+ if (!command)
425
+ continue;
426
+ k++;
427
+ if (k < window.length && MUSE_APPROVAL_STAGE.test(window[k].row))
428
+ k++;
429
+ if (k < window.length && MUSE_APPROVAL_ARGV.test(window[k].row))
430
+ k++;
431
+ const last = window[window.length - 1]?.row;
432
+ const options = (last !== undefined && MUSE_APPROVAL_FOOTER.test(last) ? window.slice(k, -1) : window.slice(k))
433
+ .map(entry => entry.row);
434
+ if (options.length !== 2 && options.length !== 3)
435
+ continue;
436
+ if (!options.every(row => MUSE_APPROVAL_OPT.test(row)))
437
+ continue;
438
+ const first = (options[0] ?? "").replace(/^[ \t]*[❯›]?[ \t]*/, "");
439
+ const second = options[1] ?? "";
440
+ if (options.length === 2) {
441
+ if (!/^1\. Yes, proceed \(y\)[ \t]*$/.test(first))
442
+ continue;
443
+ if (!/^[ \t]*[❯›]?[ \t]*2\. No, and tell Muse Code what to do differently \(esc\)[ \t]*$/.test(second))
444
+ continue;
445
+ }
446
+ else {
447
+ if (!/^1\. Allow this stage once \(y\)[ \t]*$/.test(first))
448
+ continue;
449
+ if (!/^[ \t]*[❯›]?[ \t]*2\. Always allow in this workspace: .+ \(p\)[ \t]*$/.test(second))
450
+ continue;
451
+ if (!/^[ \t]*[❯›]?[ \t]*3\. Abort the entire command \(esc\)[ \t]*$/.test(options[2] ?? ""))
452
+ continue;
453
+ }
454
+ return { head: window[h].index, command };
455
+ }
456
+ return null;
457
+ }
458
+ function museApprovalScreen(pane) {
459
+ const screen = pane.replace(/\r/g, "").split("\n");
460
+ while (screen.length && !screen[screen.length - 1].trim())
461
+ screen.pop();
462
+ const bottom = museBottomSeparator(screen);
463
+ if (bottom < 0)
464
+ return null;
465
+ // Same ownership as the quit menu (#1730 P2-1): the prompt must live in the
466
+ // current input block, and a free composer means any prompt text is history.
467
+ let top = -1;
468
+ for (let i = bottom - 1; i >= 0; i--) {
469
+ if (MUSE_SEPARATOR.test(screen[i])) {
470
+ top = i;
471
+ break;
472
+ }
473
+ }
474
+ if (museFreeComposerLayout(screen, MUSE_BUSY))
475
+ return null;
476
+ // #1715 r3: any readable composer means prompt text is a quote, not the
477
+ // live block — the live approval panes carry no ❯ composer row at all
478
+ // (inputDraft null), while a quoting draft does, even one starting with a
479
+ // blank first row. A null draft (no readable box) carries nothing to judge,
480
+ // so it still proceeds.
481
+ if (museInputBox(pane, MUSE_BUSY) !== null)
482
+ return null;
483
+ return { screen, top, bottom };
484
+ }
485
+ export function museApprovalPromptActive(pane) {
486
+ const s = museApprovalScreen(pane);
487
+ return s !== null && museApprovalBlock(s.screen, s.top, s.bottom) !== null;
488
+ }
489
+ export const MUSE_LOCAL_WORK_QUIT = /^[ \t]*Local work is still active[ \t]*$/m;
490
+ const MUSE_QUIT_HINT = /^[ \t]*Enter to confirm · Esc to cancel[ \t]*$/;
491
+ const MUSE_QUIT_OPT_1 = /^[ \t]*[❯›]?[ \t]*1\.[ \t]+Exit anyway[ \t]*$/m;
492
+ const MUSE_QUIT_OPT_2 = /^[ \t]*[❯›]?[ \t]*2\.[ \t]+Stay[ \t]*$/m;
493
+ export function museLocalWorkQuitActive(pane) {
494
+ const screen = pane.replace(/\r/g, "").split("\n");
495
+ while (screen.length && !screen[screen.length - 1].trim())
496
+ screen.pop();
497
+ const bottom = museBottomSeparator(screen);
498
+ if (bottom < 0)
499
+ return false;
500
+ // #1730 P2-1 (refined): the menu must belong to the CURRENT input block —
501
+ // between the bottom separator and the nearest separator above it. Menu
502
+ // text quoted in an older screen is history even when the current composer
503
+ // is busy with a draft, not just when it is free.
504
+ let top = -1;
505
+ for (let i = bottom - 1; i >= 0; i--) {
506
+ if (MUSE_SEPARATOR.test(screen[i])) {
507
+ top = i;
508
+ break;
509
+ }
510
+ }
511
+ // A free composer on the current screen means any menu text is history.
512
+ if (museFreeComposerLayout(screen, MUSE_BUSY))
513
+ return false;
514
+ // #1730 P2-1 r3: a populated ❯ line means any menu text is a quote, not the
515
+ // live chrome. The menu only ever answers a submitted /quit, which clears
516
+ // the box — the live capture's ❯ row is empty with the menu as chrome under
517
+ // it — while every quoting draft (r2's "cancel me", r3a's hint-carrying
518
+ // draft, r3b's menu-quoting draft) holds text on the ❯ line itself. A null
519
+ // draft (no readable box) carries nothing to judge, so it still proceeds.
520
+ // Residual: an empty ❯ followed by a verbatim menu paste is byte-identical
521
+ // to the live chrome and cannot be separated here.
522
+ const draft = museInputBox(pane, MUSE_BUSY);
523
+ if (draft !== null && draft.rows.length > 0 && draft.rows[0].trim() !== "")
524
+ return false;
525
+ // Candidates newest-first, so a stale quote above never shadows the live
526
+ // block below (or passes as live itself).
527
+ for (let h = screen.length - 1; h >= 0; h--) {
528
+ if (!MUSE_QUIT_HINT.test(screen[h]))
529
+ continue;
530
+ if (!(h > top && h < bottom))
531
+ continue;
532
+ // The block above, skipping padding blanks (the same 12-non-blank
533
+ // window as before — the live menu fits in 9), with screen indices so the
534
+ // WHOLE block is fenced to the live input box, not just the hint: the
535
+ // backward scan must not borrow the header or options from above top.
536
+ const window = [];
537
+ for (let i = h - 1; i >= 0 && window.length < 12; i--) {
538
+ if (screen[i].trim())
539
+ window.unshift({ row: screen[i], index: i });
540
+ }
541
+ if (window.length < 4)
542
+ continue;
543
+ // #1730 P2-1 r4: headers newest-first, eligible only — a scrollback header
544
+ // above top must not shadow the live header inside the box. The first
545
+ // (oldest) match can sit above top while the current menu is intact below.
546
+ for (let head = window.length - 1; head >= 0; head--) {
547
+ if (!MUSE_LOCAL_WORK_QUIT.test(window[head].row))
548
+ continue;
549
+ if (window[head].index <= top)
550
+ continue;
551
+ const block = window.slice(head).map(entry => entry.row).join("\n");
552
+ if (!MUSE_QUIT_OPT_1.test(block) || !MUSE_QUIT_OPT_2.test(block))
553
+ continue;
554
+ return true;
555
+ }
556
+ }
557
+ return false;
558
+ }
237
559
  /**
238
560
  * Meta Muse Code — the `muse` CLI (v1.3.0 verified).
239
561
  *
@@ -559,41 +881,7 @@ export class MuseBackend {
559
881
  const rows = pane.replace(/\r/g, "").split("\n");
560
882
  while (rows.length && !rows[rows.length - 1].trim())
561
883
  rows.pop();
562
- const separator = MUSE_SEPARATOR;
563
- const busyPattern = this.getBusyPattern();
564
- // The approval prompt is auto-answered rather than input-blocking, so the
565
- // idle proof itself must refuse a frame that shows dialog chrome.
566
- const dialogChrome = MUSE_DIALOG_CHROME;
567
- // Everything after the bottom separator must be the status bar.
568
- let bottomSeparator = -1;
569
- for (let i = rows.length - 2; i >= Math.max(0, rows.length - 1 - MUSE_MAX_STATUS_ROWS); i--) {
570
- if (separator.test(rows[i])) {
571
- bottomSeparator = i;
572
- break;
573
- }
574
- }
575
- if (bottomSeparator < 2)
576
- return false;
577
- if (!museStatusFooter(rows, bottomSeparator, busyPattern))
578
- return false;
579
- // Directly above it, the EMPTY live prompt. A `❯` with text is a draft
580
- // (or, higher up, a transcript echo) and never proves idle.
581
- if (!/^❯\s*$/.test(rows[bottomSeparator - 1]))
582
- return false;
583
- const topSeparator = bottomSeparator - 2;
584
- if (!separator.test(rows[topSeparator]))
585
- return false;
586
- // No busy indicator and no dialog just above the input box. The working
587
- // line (`◇ Thinking (2s · esc to interrupt)`) sits immediately above the
588
- // top separator; check 8 rows up to be safe.
589
- const above = rows.slice(Math.max(0, topSeparator - 8), topSeparator);
590
- if (above.some(r => busyPattern.test(r) || dialogChrome.test(r)))
591
- return false;
592
- // Arrow-key pickers (`/model`) are refused by their component chrome
593
- // above, not by guessing from answer text that happens to list model ids.
594
- // A `›` selection cursor is refused too; `❯` is not a cursor here — muse
595
- // echoes submitted messages with it.
596
- return !above.some(r => /^\s*›\s+\S/.test(r));
884
+ return museFreeComposerLayout(rows, this.getBusyPattern());
597
885
  }
598
886
  getErrorPatterns() {
599
887
  // NOTE: an Escape-interrupted run prints "interrupting run" in the status
@@ -659,6 +947,31 @@ export class MuseBackend {
659
947
  keys: ["1", "Enter"],
660
948
  description: "Muse tool approval — allow",
661
949
  },
950
+ {
951
+ // #1715: never answered here. Enter would take the preselected allow
952
+ // option, so the human must choose. The prompt replaces the input
953
+ // box (no draft to clear, unlike #829); deliveries simply hold.
954
+ pattern: MUSE_APPROVAL_PROMPT,
955
+ isActive: museApprovalPromptActive,
956
+ requestIdentity: museApprovalRequestIdentity,
957
+ interactionKind: "permission",
958
+ keys: [],
959
+ holdOnly: true,
960
+ blocksDelivery: true,
961
+ inputBlocked: true,
962
+ description: "Muse approval prompt — waiting for a human choice, never auto-approving",
963
+ },
964
+ {
965
+ // #1716: never answered here. A delivery's Enter would pick
966
+ // "Exit anyway"; the stop flow cancels it (Escape, then SIGTERM).
967
+ pattern: MUSE_LOCAL_WORK_QUIT,
968
+ isActive: museLocalWorkQuitActive,
969
+ keys: [],
970
+ holdOnly: true,
971
+ blocksDelivery: true,
972
+ inputBlocked: true,
973
+ description: "Muse \"Local work is still active\" quit menu — holding, never answering with Enter",
974
+ },
662
975
  ];
663
976
  }
664
977
  getContextUsage() {
@@ -726,6 +1039,19 @@ export class MuseBackend {
726
1039
  getQuitCommand() { return "/quit"; }
727
1040
  getQuitKey() { return "C-c"; }
728
1041
  getQuitKeyPresses() { return 2; }
1042
+ /** #1716: `/quit` with a background task running stops at the "Local work is still active" menu. */
1043
+ quitBlockedByDialog(pane) {
1044
+ return museLocalWorkQuitActive(pane);
1045
+ }
1046
+ /**
1047
+ * #1717: the terminals panel with a `running` task — the pause flow defers
1048
+ * an idle pause while this holds, so an AgEnD-initiated silent pause never
1049
+ * kills the user's running background work. Explicit stop/restart still
1050
+ * proceed via quitBlockedByDialog above.
1051
+ */
1052
+ backgroundWorkRunning(pane) {
1053
+ return museBackgroundWorkActive(pane);
1054
+ }
729
1055
  getCompactCommand() { return "/compact"; }
730
1056
  getClearCommand() { return "/clear"; }
731
1057
  /**