@songsid/agend 2.1.4-beta.6 → 2.1.4-beta.61

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 (185) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +143 -7
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +529 -89
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +28 -25
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +6 -4
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +16 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +506 -11
  66. package/dist/daemon.js +2162 -313
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +462 -12
  74. package/dist/fleet-manager.js +3027 -634
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  80. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +4 -1
  81. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  82. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  83. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  84. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  85. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  86. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  87. package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
  88. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  89. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  90. package/dist/general-knowledge/skills/session-management/SKILL.md +172 -8
  91. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  92. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  93. package/dist/instance-lifecycle.d.ts +99 -0
  94. package/dist/instance-lifecycle.js +357 -11
  95. package/dist/instance-lifecycle.js.map +1 -1
  96. package/dist/instructions.d.ts +12 -0
  97. package/dist/instructions.js +74 -3
  98. package/dist/instructions.js.map +1 -1
  99. package/dist/locale.js +1022 -310
  100. package/dist/locale.js.map +1 -1
  101. package/dist/login-flows.d.ts +91 -0
  102. package/dist/login-flows.js +170 -0
  103. package/dist/login-flows.js.map +1 -0
  104. package/dist/login-manager.d.ts +63 -0
  105. package/dist/login-manager.js +134 -0
  106. package/dist/login-manager.js.map +1 -0
  107. package/dist/network-family.d.ts +18 -0
  108. package/dist/network-family.js +20 -0
  109. package/dist/network-family.js.map +1 -0
  110. package/dist/outbound-handlers.d.ts +16 -1
  111. package/dist/outbound-handlers.js +174 -40
  112. package/dist/outbound-handlers.js.map +1 -1
  113. package/dist/outbound-schemas.d.ts +11 -3
  114. package/dist/outbound-schemas.js +11 -0
  115. package/dist/outbound-schemas.js.map +1 -1
  116. package/dist/pane-input-residue.d.ts +52 -0
  117. package/dist/pane-input-residue.js +107 -0
  118. package/dist/pane-input-residue.js.map +1 -0
  119. package/dist/process-memory.d.ts +6 -0
  120. package/dist/process-memory.js +45 -0
  121. package/dist/process-memory.js.map +1 -1
  122. package/dist/reply-dedup.d.ts +7 -8
  123. package/dist/reply-dedup.js +0 -0
  124. package/dist/reply-dedup.js.map +1 -1
  125. package/dist/restart-progress.d.ts +2 -0
  126. package/dist/restart-progress.js +3 -0
  127. package/dist/restart-progress.js.map +1 -1
  128. package/dist/scheduler/db.d.ts +12 -0
  129. package/dist/scheduler/db.js +59 -0
  130. package/dist/scheduler/db.js.map +1 -1
  131. package/dist/service-installer.d.ts +11 -0
  132. package/dist/service-installer.js +84 -18
  133. package/dist/service-installer.js.map +1 -1
  134. package/dist/settings-api.js +1 -1
  135. package/dist/settings-api.js.map +1 -1
  136. package/dist/setup-wizard.js +2 -2
  137. package/dist/setup-wizard.js.map +1 -1
  138. package/dist/spawn-gate.d.ts +30 -0
  139. package/dist/spawn-gate.js +79 -0
  140. package/dist/spawn-gate.js.map +1 -0
  141. package/dist/steer-capability.d.ts +1 -0
  142. package/dist/steer-capability.js +6 -0
  143. package/dist/steer-capability.js.map +1 -0
  144. package/dist/storm-window.d.ts +83 -0
  145. package/dist/storm-window.js +251 -0
  146. package/dist/storm-window.js.map +1 -0
  147. package/dist/tips.d.ts +44 -0
  148. package/dist/tips.js +355 -0
  149. package/dist/tips.js.map +1 -0
  150. package/dist/tmux-manager.d.ts +16 -0
  151. package/dist/tmux-manager.js +110 -25
  152. package/dist/tmux-manager.js.map +1 -1
  153. package/dist/tool-progress.d.ts +40 -0
  154. package/dist/tool-progress.js +289 -0
  155. package/dist/tool-progress.js.map +1 -0
  156. package/dist/topic-commands.d.ts +45 -6
  157. package/dist/topic-commands.js +394 -66
  158. package/dist/topic-commands.js.map +1 -1
  159. package/dist/transcript-monitor.d.ts +15 -2
  160. package/dist/transcript-monitor.js +63 -17
  161. package/dist/transcript-monitor.js.map +1 -1
  162. package/dist/transcript-sources.d.ts +131 -0
  163. package/dist/transcript-sources.js +580 -0
  164. package/dist/transcript-sources.js.map +1 -0
  165. package/dist/types.d.ts +11 -0
  166. package/dist/ui/dashboard.html +55 -32
  167. package/dist/ui/settings.html +200 -60
  168. package/dist/ui/view.html +147 -30
  169. package/dist/usage/format-rich.d.ts +1 -1
  170. package/dist/usage/format-rich.js +28 -24
  171. package/dist/usage/format-rich.js.map +1 -1
  172. package/dist/usage/i18n-keys.d.ts +7 -0
  173. package/dist/usage/i18n-keys.js +34 -0
  174. package/dist/usage/i18n-keys.js.map +1 -0
  175. package/dist/usage/i18n.d.ts +5 -0
  176. package/dist/usage/i18n.js +27 -0
  177. package/dist/usage/i18n.js.map +1 -0
  178. package/dist/usage/providers.d.ts +21 -0
  179. package/dist/usage/providers.js +153 -75
  180. package/dist/usage/providers.js.map +1 -1
  181. package/dist/usage/usage-api.d.ts +11 -3
  182. package/dist/usage/usage-api.js +61 -24
  183. package/dist/usage/usage-api.js.map +1 -1
  184. package/dist/workflow-templates/default.md +2 -1
  185. package/package.json +2 -2
@@ -1,4 +1,47 @@
1
1
  import { type CliBackend, type CliBackendConfig, type ErrorPattern, type RuntimeDialog, type StartupDialog } from "./types.js";
2
+ /**
3
+ * Take `lockPath` via exclusive create, recording {pid, token} so a stale lock
4
+ * can be attributed. Returns the token to release with, or null when the lock
5
+ * could not be acquired within the retry budget.
6
+ *
7
+ * A held lock is NEVER auto-reclaimed. Reclaiming under concurrency cannot be
8
+ * made atomic with the primitives available (rename/link both leave a window
9
+ * where a reclaimed-and-reacquired live lock gets stolen), and every claude.json
10
+ * update is best-effort with a fallback — a skipped trust write is picked up by
11
+ * the startup-dialog dismisser. A lock left by a crashed holder therefore means
12
+ * skipped updates until it is removed manually (delete the .agend.lock file);
13
+ * the crash window is one writeFileSync inside a ~1ms critical section. When
14
+ * the recorded owner PID is verifiably dead we skip immediately instead of
15
+ * burning the whole retry budget waiting for a release that can never come.
16
+ */
17
+ export declare function acquireClaudeJsonLock(lockPath: string, maxAttempts?: number): string | null;
18
+ /** Release a lock taken by acquireClaudeJsonLock, only if we still own it. */
19
+ export declare function releaseClaudeJsonLock(lockPath: string, token: string): void;
20
+ /**
21
+ * Claude's session-resume prompt with the cursor on the destructive default
22
+ * (captured from the 2.1.261 binary: "❯ 1. Resume from summary (recommended)" /
23
+ * " 2. Resume full session as-is"). Anchored to the selector + option number so
24
+ * transcript prose quoting the sentence never matches.
25
+ */
26
+ export declare const CLAUDE_RESUME_PROMPT_DEFAULT: RegExp;
27
+ /**
28
+ * The resume prompt's two-option menu in ANY arrangement: two consecutive
29
+ * numbered "Resume …" rows. Matches the default shape too, so it must be
30
+ * ordered AFTER the exact entry; on its own it means "a resume menu we do not
31
+ * know how to navigate" — hold, never press.
32
+ */
33
+ export declare const CLAUDE_RESUME_PROMPT_MENU: RegExp;
34
+ /**
35
+ * Is the resume menu the CURRENT interactive region of the pane, and is the
36
+ * cursor on the destructive default? Bottom-anchored on purpose: the LAST pair
37
+ * of option rows must be followed only by the dialog's own footer (at most two
38
+ * rows) — a quoted capture of the menu in a transcript is followed by more
39
+ * transcript and, above all, by the real `❯` input row, so it is not active.
40
+ */
41
+ export declare function claudeResumeMenuState(pane: string): {
42
+ active: boolean;
43
+ defaultCursor: boolean;
44
+ };
2
45
  export declare class ClaudeCodeBackend implements CliBackend {
3
46
  private instanceDir;
4
47
  readonly binaryName = "claude";
@@ -23,20 +66,28 @@ export declare class ClaudeCodeBackend implements CliBackend {
23
66
  getReadyPattern(): RegExp;
24
67
  /**
25
68
  * The live spinner line, which is on screen only while generating. Captured from
26
- * two running panes:
69
+ * running panes and their pipe-pane recordings:
27
70
  *
28
71
  * working → `✢ Accomplishing… (11m 26s · ↓ 38.0k tokens)`
29
72
  * idle → `✻ Worked for 6m 49s`
30
73
  *
31
74
  * Both panes also showed `❯` and the `ok` statusline, which is why neither of
32
- * those can carry the distinction. The discriminator is the ellipsis followed by
33
- * a parenthesised live elapsed counter; the completed line is past tense with no
34
- * `…` and no counter. The verb and the glyph both rotate, so neither is matched.
75
+ * those can carry the distinction. The discriminator is the rotating glyph plus
76
+ * Claude's one-word spinner label ending in an ellipsis. The completed line is
77
+ * past tense with no ellipsis. The verb and glyph both rotate, so neither
78
+ * specific value is matched.
35
79
  *
36
- * The leading class is "any non-ASCII symbol, or an asterisk". It used to be
37
- * `[^\x00-\x7F\p{L}\p{N}\s]` alone, requiring a non-ASCII glyphbut the
38
- * animation cycles through six frames and one of them is a plain ASCII
39
- * asterisk. Sampling a continuously working pane 50 times at 250ms:
80
+ * Claude Code 2.1.239 paints that line incrementally. For roughly the first
81
+ * three seconds and briefly during later repaints capture-pane can see only
82
+ * `* Nesting…` before `(3s · 9 tokens)` is appended. It also emits hyphenated
83
+ * labels such as `Razzle-dazzling…` and `Sock-hopping…`. Requiring the elapsed
84
+ * counter and a letters-only verb made both real shapes look idle. Since `❯`
85
+ * remains visible throughout generation, a settled capture in either shape
86
+ * retired Cancel and the progress bubble while the turn was still running.
87
+ *
88
+ * The animation cycles through six measured frames, including one plain ASCII
89
+ * asterisk. The older leading class required a non-ASCII glyph, so sampling a
90
+ * continuously working pane 50 times at 250ms produced:
40
91
  *
41
92
  * ✻ 11/11 ✽ 10/10 ✢ 10/10 · 9/9 ✶ 4/4 * 0/6 ← U+002A never matched
42
93
  *
@@ -46,19 +97,13 @@ export declare class ClaudeCodeBackend implements CliBackend {
46
97
  * retired cancel buttons mid-turn. 12% of frames, so it hit some turns and not
47
98
  * others.
48
99
  *
49
- * `*` is spelled out rather than opening the class to all ASCII punctuation:
50
- * `> quoted… (2s)` in agent output would otherwise read as a spinner, and a
51
- * false positive on a *stable* pane pins the instance in `working` forever.
52
- * If a future release adds another ASCII frame this pattern misses it again —
53
- * but a missed frame can no longer flip the state on its own, because the
54
- * state machine only decides on patterns once output has settled.
55
- *
56
- * Deliberately narrow, because the cost is asymmetric. Missing the spinner just
57
- * restores today's behaviour; matching prose that happens to sit on a *stable*
58
- * pane would hold the instance in `working` forever — no auto-pause, no cancel
59
- * button retirement, and eventually a bogus hang alert. So the line must start
60
- * with a non-ASCII, non-letter glyph and carry a single word ending in `…`:
61
- * `- Something… (5s)` and `I waited… (30s) for the build` are both rejected.
100
+ * Deliberately narrow, because the cost is asymmetric. Matching prose that
101
+ * happens to sit on a *stable* pane would hold the instance in `working`
102
+ * forever no auto-pause, no cancel-button retirement, and eventually a bogus
103
+ * hang alert. Therefore this accepts only Claude's six measured spinner glyphs,
104
+ * exactly one word (optionally hyphenated), and the Unicode ellipsis. Markdown
105
+ * prose such as `* bullet point…`, `- Something…`, and a completed
106
+ * `✻ Worked for 6m 49s` remain rejected.
62
107
  *
63
108
  * Note when this actually decides anything: while the CLI really is generating,
64
109
  * the elapsed counter ticks, the pane changes, and motion already reports
@@ -79,14 +124,33 @@ export declare class ClaudeCodeBackend implements CliBackend {
79
124
  getEffortLevels(): string[];
80
125
  getModelSwitchStrategy(): "runtime" | "restart";
81
126
  listModels(): Promise<import("./types.js").ModelOption[]>;
127
+ /**
128
+ * Full account catalog from `GET /v1/models` using the same OAuth token the
129
+ * usage panel resolves. Each model also gets an `<id>[1m]` 1M-context
130
+ * variant: the API cannot say which plans may use it, so the variants are
131
+ * offered and a wrong pick surfaces through the model_error pattern above.
132
+ * Any failure (no token, network, non-200) degrades to [] — the static
133
+ * aliases from listModels() always remain available.
134
+ */
135
+ listApiModels(): Promise<import("./types.js").ModelOption[]>;
82
136
  probeCLIEnv(): Promise<{
83
137
  version: string | undefined;
84
138
  models: import("./types.js").ModelOption[];
139
+ apiModels: import("./types.js").ModelOption[];
85
140
  }>;
86
141
  cleanup(_config: CliBackendConfig): void;
87
- /** Pre-approve ANTHROPIC_API_KEY in ~/.claude.json to skip the interactive prompt */
142
+ /**
143
+ * Pre-accept Claude Code's workspace trust dialog for this working directory.
144
+ * The interactive TUI blocks a fresh workspace on "Do you trust the files in
145
+ * this folder?" and nothing in tmux answers it, so the process exits and the
146
+ * instance crash-loops. No CLI flag skips it interactively (verified on
147
+ * 2.1.250 — only -p / non-TTY stdout bypass the dialog); the recognised
148
+ * signal is projects[<cwd>].hasTrustDialogAccepted in claude.json.
149
+ */
150
+ preTrust(workingDirectory: string): void;
151
+ /** Pre-approve ANTHROPIC_API_KEY in claude.json to skip the interactive prompt */
88
152
  private preApproveApiKey;
89
- /** Check if user has an active OAuth session in ~/.claude.json */
153
+ /** Check if user has an active OAuth session in claude.json */
90
154
  private hasOAuthSession;
91
155
  private writeStatusLineScript;
92
156
  }
@@ -1,5 +1,5 @@
1
- import { join, resolve } from "node:path";
2
- import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { resolveBinary, shellQuote, validateModel, warnIfModelMismatch } from "./types.js";
5
5
  /** Mirror Claude Code's ~/.claude/projects key for a working directory. */
@@ -23,6 +23,169 @@ function claudeProjectKey(cwd) {
23
23
  }
24
24
  return `${sanitized.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
25
25
  }
26
+ /** Claude Code's top-level config file, honoring CLAUDE_CONFIG_DIR. */
27
+ function claudeJsonPath() {
28
+ const configDir = process.env.CLAUDE_CONFIG_DIR?.trim();
29
+ return configDir ? join(configDir, ".claude.json") : join(homedir(), ".claude.json");
30
+ }
31
+ function sleepSync(ms) {
32
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
33
+ }
34
+ /**
35
+ * Take `lockPath` via exclusive create, recording {pid, token} so a stale lock
36
+ * can be attributed. Returns the token to release with, or null when the lock
37
+ * could not be acquired within the retry budget.
38
+ *
39
+ * A held lock is NEVER auto-reclaimed. Reclaiming under concurrency cannot be
40
+ * made atomic with the primitives available (rename/link both leave a window
41
+ * where a reclaimed-and-reacquired live lock gets stolen), and every claude.json
42
+ * update is best-effort with a fallback — a skipped trust write is picked up by
43
+ * the startup-dialog dismisser. A lock left by a crashed holder therefore means
44
+ * skipped updates until it is removed manually (delete the .agend.lock file);
45
+ * the crash window is one writeFileSync inside a ~1ms critical section. When
46
+ * the recorded owner PID is verifiably dead we skip immediately instead of
47
+ * burning the whole retry budget waiting for a release that can never come.
48
+ */
49
+ export function acquireClaudeJsonLock(lockPath, maxAttempts = 40) {
50
+ const token = `${process.pid}.${Math.random().toString(16).slice(2)}`;
51
+ const payload = JSON.stringify({ pid: process.pid, token });
52
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
53
+ try {
54
+ writeFileSync(lockPath, payload, { flag: "wx", mode: 0o600 });
55
+ return token;
56
+ }
57
+ catch { /* already held — fall through to the holder check */ }
58
+ let ownerPid = null;
59
+ try {
60
+ ownerPid = Number(JSON.parse(readFileSync(lockPath, "utf-8")).pid) || null;
61
+ }
62
+ catch {
63
+ continue; /* vanished (retry the create now) or content not written yet (retry) */
64
+ }
65
+ if (ownerPid !== null) {
66
+ try {
67
+ process.kill(ownerPid, 0);
68
+ }
69
+ catch (err) {
70
+ // ESRCH = the holder is dead and will never release — give up now.
71
+ // EPERM means alive under another user: keep waiting like any holder.
72
+ if (err.code === "ESRCH")
73
+ return null;
74
+ }
75
+ }
76
+ sleepSync(25);
77
+ }
78
+ return null;
79
+ }
80
+ /** Release a lock taken by acquireClaudeJsonLock, only if we still own it. */
81
+ export function releaseClaudeJsonLock(lockPath, token) {
82
+ try {
83
+ const holder = JSON.parse(readFileSync(lockPath, "utf-8"));
84
+ if (holder.token === token)
85
+ unlinkSync(lockPath);
86
+ }
87
+ catch { /* already gone or replaced — nothing of ours to release */ }
88
+ }
89
+ /**
90
+ * Read-modify-write claude.json under a lock file, so concurrent instance
91
+ * spawns don't drop each other's updates, then replace it via atomic rename so
92
+ * a reader never sees a torn file. The file and its lock are created 0600 —
93
+ * claude.json holds OAuth account data, and the rename would otherwise replace
94
+ * the CLI's own 0600 file with the temp file's default 0644/umask mode.
95
+ * `mutate` returns false to skip the write. If the lock cannot be acquired the
96
+ * update is SKIPPED, never performed unlocked (last-write-wins against a
97
+ * concurrent holder could drop that holder's entry); an existing-but-
98
+ * unparseable file is likewise left untouched — never clobber the user's whole
99
+ * config over a parse error. Both give up to the getStartupDialogs() fallback.
100
+ */
101
+ function updateClaudeJson(mutate) {
102
+ const path = claudeJsonPath();
103
+ // The parent must exist before both the lock create and the rename — on a
104
+ // fresh machine (no ~/.claude.json yet) an ENOENT here previously made every
105
+ // lock attempt fail, silently degrading to unlocked last-write-wins.
106
+ mkdirSync(dirname(path), { recursive: true });
107
+ const lockPath = `${path}.agend.lock`;
108
+ const token = acquireClaudeJsonLock(lockPath);
109
+ if (token === null)
110
+ return;
111
+ try {
112
+ let cfg = {};
113
+ let raw = null;
114
+ try {
115
+ raw = readFileSync(path, "utf-8");
116
+ }
117
+ catch { /* missing file — create a minimal one */ }
118
+ if (raw !== null) {
119
+ try {
120
+ const parsed = JSON.parse(raw);
121
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
122
+ return;
123
+ cfg = parsed;
124
+ }
125
+ catch {
126
+ return;
127
+ }
128
+ }
129
+ if (!mutate(cfg))
130
+ return;
131
+ const tempPath = join(dirname(path), `.claude.json.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
132
+ try {
133
+ writeFileSync(tempPath, JSON.stringify(cfg, null, 2), { flag: "wx", mode: 0o600 });
134
+ chmodSync(tempPath, 0o600); // mode above is masked by umask; make 0600 unconditional
135
+ renameSync(tempPath, path);
136
+ }
137
+ finally {
138
+ try {
139
+ unlinkSync(tempPath);
140
+ }
141
+ catch { /* already renamed */ }
142
+ }
143
+ }
144
+ finally {
145
+ releaseClaudeJsonLock(lockPath, token);
146
+ }
147
+ }
148
+ /**
149
+ * Claude's session-resume prompt with the cursor on the destructive default
150
+ * (captured from the 2.1.261 binary: "❯ 1. Resume from summary (recommended)" /
151
+ * " 2. Resume full session as-is"). Anchored to the selector + option number so
152
+ * transcript prose quoting the sentence never matches.
153
+ */
154
+ export const CLAUDE_RESUME_PROMPT_DEFAULT = /^[ \t]*[❯›][ \t]*1\.[ \t]*Resume from summary \(recommended\)/m;
155
+ /**
156
+ * The resume prompt's two-option menu in ANY arrangement: two consecutive
157
+ * numbered "Resume …" rows. Matches the default shape too, so it must be
158
+ * ordered AFTER the exact entry; on its own it means "a resume menu we do not
159
+ * know how to navigate" — hold, never press.
160
+ */
161
+ export const CLAUDE_RESUME_PROMPT_MENU = /^[ \t]*[❯›]?[ \t]*\d\.[ \t]*Resume (?:full session as-is|from summary)[^\n]*\n[ \t]*[❯›]?[ \t]*\d\.[ \t]*Resume (?:full session as-is|from summary)/m;
162
+ const RESUME_OPTION_ROW = /^[ \t]*[❯›]?[ \t]*\d\.[ \t]*Resume (?:full session as-is|from summary)/;
163
+ const RESUME_DIALOG_FOOTER = /Enter to confirm|Esc to cancel|↑↓|to select|to navigate/i;
164
+ /**
165
+ * Is the resume menu the CURRENT interactive region of the pane, and is the
166
+ * cursor on the destructive default? Bottom-anchored on purpose: the LAST pair
167
+ * of option rows must be followed only by the dialog's own footer (at most two
168
+ * rows) — a quoted capture of the menu in a transcript is followed by more
169
+ * transcript and, above all, by the real `❯` input row, so it is not active.
170
+ */
171
+ export function claudeResumeMenuState(pane) {
172
+ const rows = pane.replace(/\r/g, "").split("\n");
173
+ let start = -1;
174
+ for (let i = rows.length - 2; i >= 0; i--) {
175
+ if (RESUME_OPTION_ROW.test(rows[i]) && RESUME_OPTION_ROW.test(rows[i + 1])) {
176
+ start = i;
177
+ break;
178
+ }
179
+ }
180
+ if (start < 0)
181
+ return { active: false, defaultCursor: false };
182
+ const trailing = rows.slice(start + 2).filter(r => r.trim().length > 0);
183
+ const active = trailing.length <= 2 && trailing.every(r => RESUME_DIALOG_FOOTER.test(r));
184
+ const defaultCursor = /^[ \t]*[❯›][ \t]*1\.[ \t]*Resume from summary \(recommended\)/.test(rows[start]);
185
+ return { active, defaultCursor };
186
+ }
187
+ const resumeDefaultActive = (pane) => { const s = claudeResumeMenuState(pane); return s.active && s.defaultCursor; };
188
+ const resumeMenuActive = (pane) => claudeResumeMenuState(pane).active;
26
189
  export class ClaudeCodeBackend {
27
190
  instanceDir;
28
191
  binaryName = "claude";
@@ -58,7 +221,7 @@ export class ClaudeCodeBackend {
58
221
  if (config.model) {
59
222
  const model = validateModel(config.model);
60
223
  warnIfModelMismatch("claude-code", model);
61
- cmd += ` --model ${model}`;
224
+ cmd += ` --model ${shellQuote(model)}`;
62
225
  }
63
226
  // Additive system prompt: append fleet instructions without overriding Claude's built-in prompt
64
227
  const instrFile = join(this.instanceDir, "fleet-instructions.md");
@@ -123,20 +286,28 @@ export class ClaudeCodeBackend {
123
286
  }
124
287
  /**
125
288
  * The live spinner line, which is on screen only while generating. Captured from
126
- * two running panes:
289
+ * running panes and their pipe-pane recordings:
127
290
  *
128
291
  * working → `✢ Accomplishing… (11m 26s · ↓ 38.0k tokens)`
129
292
  * idle → `✻ Worked for 6m 49s`
130
293
  *
131
294
  * Both panes also showed `❯` and the `ok` statusline, which is why neither of
132
- * those can carry the distinction. The discriminator is the ellipsis followed by
133
- * a parenthesised live elapsed counter; the completed line is past tense with no
134
- * `…` and no counter. The verb and the glyph both rotate, so neither is matched.
295
+ * those can carry the distinction. The discriminator is the rotating glyph plus
296
+ * Claude's one-word spinner label ending in an ellipsis. The completed line is
297
+ * past tense with no ellipsis. The verb and glyph both rotate, so neither
298
+ * specific value is matched.
135
299
  *
136
- * The leading class is "any non-ASCII symbol, or an asterisk". It used to be
137
- * `[^\x00-\x7F\p{L}\p{N}\s]` alone, requiring a non-ASCII glyphbut the
138
- * animation cycles through six frames and one of them is a plain ASCII
139
- * asterisk. Sampling a continuously working pane 50 times at 250ms:
300
+ * Claude Code 2.1.239 paints that line incrementally. For roughly the first
301
+ * three seconds and briefly during later repaints capture-pane can see only
302
+ * `* Nesting…` before `(3s · 9 tokens)` is appended. It also emits hyphenated
303
+ * labels such as `Razzle-dazzling…` and `Sock-hopping…`. Requiring the elapsed
304
+ * counter and a letters-only verb made both real shapes look idle. Since `❯`
305
+ * remains visible throughout generation, a settled capture in either shape
306
+ * retired Cancel and the progress bubble while the turn was still running.
307
+ *
308
+ * The animation cycles through six measured frames, including one plain ASCII
309
+ * asterisk. The older leading class required a non-ASCII glyph, so sampling a
310
+ * continuously working pane 50 times at 250ms produced:
140
311
  *
141
312
  * ✻ 11/11 ✽ 10/10 ✢ 10/10 · 9/9 ✶ 4/4 * 0/6 ← U+002A never matched
142
313
  *
@@ -146,19 +317,13 @@ export class ClaudeCodeBackend {
146
317
  * retired cancel buttons mid-turn. 12% of frames, so it hit some turns and not
147
318
  * others.
148
319
  *
149
- * `*` is spelled out rather than opening the class to all ASCII punctuation:
150
- * `> quoted… (2s)` in agent output would otherwise read as a spinner, and a
151
- * false positive on a *stable* pane pins the instance in `working` forever.
152
- * If a future release adds another ASCII frame this pattern misses it again —
153
- * but a missed frame can no longer flip the state on its own, because the
154
- * state machine only decides on patterns once output has settled.
155
- *
156
- * Deliberately narrow, because the cost is asymmetric. Missing the spinner just
157
- * restores today's behaviour; matching prose that happens to sit on a *stable*
158
- * pane would hold the instance in `working` forever — no auto-pause, no cancel
159
- * button retirement, and eventually a bogus hang alert. So the line must start
160
- * with a non-ASCII, non-letter glyph and carry a single word ending in `…`:
161
- * `- Something… (5s)` and `I waited… (30s) for the build` are both rejected.
320
+ * Deliberately narrow, because the cost is asymmetric. Matching prose that
321
+ * happens to sit on a *stable* pane would hold the instance in `working`
322
+ * forever no auto-pause, no cancel-button retirement, and eventually a bogus
323
+ * hang alert. Therefore this accepts only Claude's six measured spinner glyphs,
324
+ * exactly one word (optionally hyphenated), and the Unicode ellipsis. Markdown
325
+ * prose such as `* bullet point…`, `- Something…`, and a completed
326
+ * `✻ Worked for 6m 49s` remain rejected.
162
327
  *
163
328
  * Note when this actually decides anything: while the CLI really is generating,
164
329
  * the elapsed counter ticks, the pane changes, and motion already reports
@@ -166,7 +331,7 @@ export class ClaudeCodeBackend {
166
331
  * an in-progress spinner — which is exactly the hang this is meant to surface.
167
332
  */
168
333
  getBusyPattern() {
169
- return /^[ \t]*(?:[^\x00-\x7F\p{L}\p{N}\s]|\*)\s+\p{L}+…\s*\(\d+[hms]\b/mu;
334
+ return /^[ \t]*[✻✽✢·✶*][ \t]+\p{L}+(?:-\p{L}+)*…(?:[ \t]+\([^\n]*)?[ \t]*$/mu;
170
335
  }
171
336
  getContextUsage() {
172
337
  try {
@@ -192,18 +357,63 @@ export class ClaudeCodeBackend {
192
357
  getErrorPatterns() {
193
358
  return [
194
359
  { pattern: /API Error: Rate limit/i, type: "rate_limit", action: "failover", message: "API rate limit reached" },
195
- { pattern: /Login expired|Not logged in|Please run \/login/i, type: "auth_error", action: "notify", message: "Claude login expired needs re-login (/login)" },
360
+ // pause (not just notify): an auth-expired CLI keeps accepting queued work
361
+ // it can never answer. The pause is lifted by /login's post-success
362
+ // restart, and the lifecycle double-checks with the token-free probe first.
363
+ { pattern: /Login expired|Not logged in|Please run \/login/i, type: "auth_error", action: "pause", message: "Claude login expired — needs re-login (/login)" },
196
364
  { pattern: /API Error: Authentication/i, type: "auth_error", action: "pause", message: "Authentication error" },
365
+ // A bad /model choice previously failed silently (live-reported): the CLI
366
+ // prints these and idles without answering. notify (not restart): the
367
+ // session is healthy, only the model selection needs changing.
368
+ {
369
+ // Claude 2.1.250 emits `unrecognized_model` for an unknown ID and
370
+ // `model_access` / the plan sentence when the ID exists but the
371
+ // account is not entitled to it. Keep the prose alternatives narrow:
372
+ // the error monitor also sees normal conversation in pane scrollback.
373
+ pattern: /\[claude-code:(?:unrecognized_model|model_access)\]|There's an issue with the selected model|\bis not available on your [^\n]{0,80},\s*or ask your admin to enable this model/i,
374
+ type: "model_error",
375
+ action: "notify",
376
+ message: "Selected Claude model unavailable — Claude Code may be using a fallback; use /model to choose another",
377
+ },
197
378
  { pattern: /API Error: Overloaded/i, type: "rate_limit", action: "notify", message: "API overloaded" },
198
379
  { pattern: /credit balance is too low/i, type: "quota", action: "pause", message: "Insufficient API credits" },
199
380
  ];
200
381
  }
201
382
  getStartupDialogs() {
202
383
  return [
384
+ // A corrupt claude.json shows a modal whose only options are "1. Exit and
385
+ // fix manually" / "2. Reset with default configuration" (captured live on
386
+ // 2.1.250). Neither is dismissable in a useful direction: Enter exits
387
+ // (crash loop), reset wipes the user's config. The modal's ❯ selector
388
+ // would satisfy the ready pattern, so without this entry the instance
389
+ // reports ready and queued messages get typed into the modal. Must stay
390
+ // FIRST so no dismiss pattern grabs the pane before it.
391
+ {
392
+ pattern: /Configuration error[\s\S]{0,800}?contains invalid JSON/,
393
+ keys: [],
394
+ description: "Claude corrupt-config modal — not dismissable, reporting config_error",
395
+ fatal: { type: "config_error", action: "pause", message: "claude.json is corrupt — Claude Code cannot start. Fix or remove the file (Claude backs the corrupt copy up under <config>/backups/), then wake the instance." },
396
+ },
203
397
  // Session resume prompt must be checked BEFORE ready pattern, because ❯ in
204
398
  // "❯ 1. Resume from summary" would falsely match the ready pattern /❯/.
205
- { pattern: /Resume from summary \(recommended\)/, keys: ["Down", "Enter"], description: "Claude session resume prompt — select 'Resume full session as-is'" },
399
+ // Only when the cursor is verifiably on option 1 (" 1. Resume from
400
+ // summary (recommended)") do we know that Down+Enter lands on "Resume
401
+ // full session as-is". Any other shape is held by the guard below.
402
+ { pattern: CLAUDE_RESUME_PROMPT_DEFAULT, isActive: resumeDefaultActive, keys: ["Down", "Enter"], description: "Claude session resume prompt — select 'Resume full session as-is'", blocksDelivery: true },
403
+ // The same prompt in a shape we do not know how to navigate (cursor
404
+ // elsewhere, wording changed, options reordered): recognised by its
405
+ // two-option menu structure, never answered — a blind Enter would take
406
+ // the default and drop the full context. Hold the pane, report for a human.
407
+ { pattern: CLAUDE_RESUME_PROMPT_MENU, isActive: resumeMenuActive, keys: [], holdOnly: true, blocksDelivery: true, description: "Claude session resume prompt (unrecognised variant) — holding for a human, never auto-selecting" },
206
408
  { pattern: /[❯›]\s*\d+\.\s*No/m, keys: ["Down", "Enter"], description: "Claude 'No, exit' confirmation — navigate to Yes" },
409
+ // The 2.1.250 workspace-trust dialog has no numbered options — the cursor
410
+ // sits on "❯ No, exit" above "Yes, I trust this folder" (captured live).
411
+ // It must be matched BEFORE the generic /I trust/ Enter fallback below:
412
+ // that pattern also matches this screen's "Yes, I trust this folder" text,
413
+ // and a bare Enter there confirms "No, exit" — the dialog fallback itself
414
+ // used to exit the CLI. preTrust() normally prevents the dialog entirely;
415
+ // this is the recovery path when the config write was skipped.
416
+ { pattern: /[❯›]\s*No, exit/m, keys: ["Down", "Enter"], description: "Claude workspace trust dialog — navigate to 'Yes, I trust this folder'" },
207
417
  { pattern: /I accept|I trust/i, keys: ["Enter"], description: "Claude 'Yes, I accept' trust dialog" },
208
418
  { pattern: /Resume Session/i, keys: ["Escape"], description: "Claude resume session picker — start fresh" },
209
419
  ];
@@ -211,11 +421,24 @@ export class ClaudeCodeBackend {
211
421
  getRuntimeDialogs() {
212
422
  return [
213
423
  {
214
- // Claude Code shows a session resume prompt when session is old/large.
424
+ // Claude Code shows a session resume prompt when session is old/large
425
+ // sometimes only after loading the session, i.e. after the startup scan
426
+ // has already moved on, which is why it is in the runtime table too.
215
427
  // Default cursor is on summary; move down to preserve the full context.
216
- pattern: /Resume from summary \(recommended\)/,
428
+ pattern: CLAUDE_RESUME_PROMPT_DEFAULT,
429
+ isActive: resumeDefaultActive,
217
430
  keys: ["Down", "Enter"],
218
431
  description: "Claude session resume prompt — select 'Resume full session as-is'",
432
+ blocksDelivery: true,
433
+ },
434
+ // Same variant guard as getStartupDialogs: recognise, hold, report — never Enter.
435
+ {
436
+ pattern: CLAUDE_RESUME_PROMPT_MENU,
437
+ isActive: resumeMenuActive,
438
+ keys: [],
439
+ holdOnly: true,
440
+ blocksDelivery: true,
441
+ description: "Claude session resume prompt (unrecognised variant) — holding for a human, never auto-selecting",
219
442
  },
220
443
  ];
221
444
  }
@@ -241,39 +464,131 @@ export class ClaudeCodeBackend {
241
464
  { id: "Fable", label: "Fable", description: "Claude Fable 5" },
242
465
  ];
243
466
  }
467
+ /**
468
+ * Full account catalog from `GET /v1/models` using the same OAuth token the
469
+ * usage panel resolves. Each model also gets an `<id>[1m]` 1M-context
470
+ * variant: the API cannot say which plans may use it, so the variants are
471
+ * offered and a wrong pick surfaces through the model_error pattern above.
472
+ * Any failure (no token, network, non-200) degrades to [] — the static
473
+ * aliases from listModels() always remain available.
474
+ */
475
+ async listApiModels() {
476
+ try {
477
+ const { getClaudeOAuthToken } = await import("../usage/providers.js");
478
+ const token = await getClaudeOAuthToken();
479
+ if (!token)
480
+ return [];
481
+ const controller = new AbortController();
482
+ const timer = setTimeout(() => controller.abort(), 8_000);
483
+ timer.unref?.();
484
+ const res = await fetch("https://api.anthropic.com/v1/models?limit=100", {
485
+ headers: {
486
+ Authorization: `Bearer ${token}`,
487
+ "anthropic-version": "2023-06-01",
488
+ "anthropic-beta": "oauth-2025-04-20",
489
+ },
490
+ signal: controller.signal,
491
+ });
492
+ clearTimeout(timer);
493
+ if (!res.ok)
494
+ return [];
495
+ const body = await res.json();
496
+ const options = [];
497
+ for (const model of body.data ?? []) {
498
+ if (!model.id)
499
+ continue;
500
+ options.push({ id: model.id, label: model.id, ...(model.display_name ? { description: model.display_name } : {}) });
501
+ options.push({ id: `${model.id}[1m]`, label: `${model.id}[1m]`, description: "1M context — plan-dependent" });
502
+ }
503
+ return options;
504
+ }
505
+ catch {
506
+ return [];
507
+ }
508
+ }
244
509
  async probeCLIEnv() {
245
510
  const { probeCliVersion } = await import("./types.js");
246
- return { version: probeCliVersion(this.binaryPath), models: await this.listModels() };
511
+ return {
512
+ version: probeCliVersion(this.binaryPath),
513
+ models: await this.listModels(),
514
+ apiModels: await this.listApiModels(),
515
+ };
247
516
  }
248
517
  cleanup(_config) {
249
518
  // mcp-config.json is in instance dir, cleaned up when instance is deleted
250
519
  }
251
- /** Pre-approve ANTHROPIC_API_KEY in ~/.claude.json to skip the interactive prompt */
520
+ /**
521
+ * Pre-accept Claude Code's workspace trust dialog for this working directory.
522
+ * The interactive TUI blocks a fresh workspace on "Do you trust the files in
523
+ * this folder?" and nothing in tmux answers it, so the process exits and the
524
+ * instance crash-loops. No CLI flag skips it interactively (verified on
525
+ * 2.1.250 — only -p / non-TTY stdout bypass the dialog); the recognised
526
+ * signal is projects[<cwd>].hasTrustDialogAccepted in claude.json.
527
+ */
528
+ preTrust(workingDirectory) {
529
+ const cwd = workingDirectory?.trim();
530
+ if (!cwd)
531
+ return;
532
+ const literal = resolve(cwd);
533
+ let canonical = literal;
534
+ try {
535
+ canonical = realpathSync(literal);
536
+ }
537
+ catch { /* not created yet — trust the literal path */ }
538
+ // Trust both spellings when they differ: Claude keys projects by the cwd's
539
+ // realpath but falls back to the unresolved absolute path (see
540
+ // claudeProjectKey above), and which one the TUI sees depends on how tmux
541
+ // entered the directory.
542
+ const paths = canonical === literal ? [canonical] : [canonical, literal];
543
+ try {
544
+ updateClaudeJson(cfg => {
545
+ let projects = cfg.projects;
546
+ if (!projects || typeof projects !== "object" || Array.isArray(projects)) {
547
+ projects = {};
548
+ cfg.projects = projects;
549
+ }
550
+ let changed = false;
551
+ for (const p of paths) {
552
+ const existing = projects[p];
553
+ const entry = existing && typeof existing === "object" && !Array.isArray(existing)
554
+ ? existing
555
+ : {};
556
+ if (entry.hasTrustDialogAccepted === true)
557
+ continue;
558
+ entry.hasTrustDialogAccepted = true;
559
+ projects[p] = entry;
560
+ changed = true;
561
+ }
562
+ return changed;
563
+ });
564
+ }
565
+ catch { /* best effort — startup dialog auto-dismiss remains the fallback */ }
566
+ }
567
+ /** Pre-approve ANTHROPIC_API_KEY in claude.json to skip the interactive prompt */
252
568
  preApproveApiKey(_config) {
253
569
  const apiKey = process.env.ANTHROPIC_API_KEY;
254
570
  if (!apiKey)
255
571
  return;
256
572
  const fingerprint = apiKey.length > 20 ? apiKey.slice(-20) : apiKey;
257
- const claudeJsonPath = join(homedir(), ".claude.json");
258
- let claudeCfg = {};
259
573
  try {
260
- claudeCfg = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
261
- }
262
- catch { /* new file or parse error */ }
263
- const existing = claudeCfg.customApiKeyResponses;
264
- const approved = existing?.approved ?? [];
265
- if (!approved.includes(fingerprint)) {
266
- claudeCfg.customApiKeyResponses = {
267
- approved: [...approved, fingerprint],
268
- rejected: existing?.rejected ?? [],
269
- };
270
- writeFileSync(claudeJsonPath, JSON.stringify(claudeCfg, null, 2));
574
+ updateClaudeJson(cfg => {
575
+ const existing = cfg.customApiKeyResponses;
576
+ const approved = existing?.approved ?? [];
577
+ if (approved.includes(fingerprint))
578
+ return false;
579
+ cfg.customApiKeyResponses = {
580
+ approved: [...approved, fingerprint],
581
+ rejected: existing?.rejected ?? [],
582
+ };
583
+ return true;
584
+ });
271
585
  }
586
+ catch { /* best effort — the startup prompt remains the fallback */ }
272
587
  }
273
- /** Check if user has an active OAuth session in ~/.claude.json */
588
+ /** Check if user has an active OAuth session in claude.json */
274
589
  hasOAuthSession() {
275
590
  try {
276
- const cfg = JSON.parse(readFileSync(join(homedir(), ".claude.json"), "utf-8"));
591
+ const cfg = JSON.parse(readFileSync(claudeJsonPath(), "utf-8"));
277
592
  return !!cfg.oauthAccount?.accountUuid;
278
593
  }
279
594
  catch {