skimpyclaw 0.3.14 → 0.4.0

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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  // Code Agent Utilities
2
2
  import { execSync } from 'child_process';
3
+ import { existsSync, readFileSync } from 'fs';
3
4
  import { resolve, join } from 'path';
4
5
  import { homedir } from 'os';
5
6
  import { buildValidationCommand } from './executor.js';
@@ -21,9 +22,25 @@ function isCommandAvailable(name) {
21
22
  return false;
22
23
  }
23
24
  }
24
- const CLAUDE_CLI_PATH = resolveCliPath('claude');
25
+ export const CLAUDE_CLI_PATH = resolveCliPath('claude');
25
26
  const CODEX_CLI_PATH = resolveCliPath('codex');
26
- const KIMI_CLI_PATH = resolveCliPath('kimi');
27
+ const DEFAULT_CODEX_HOME = join(homedir(), '.codex');
28
+ /**
29
+ * Prepare env for spawning a coding-agent CLI (claude/codex):
30
+ * - Drops CLAUDECODE so nested `claude` invocations start cleanly.
31
+ * - Drops GH_TOKEN/GITHUB_TOKEN so `gh` falls back to keychain auth instead
32
+ * of a stale process token.
33
+ * - Pins Codex to the standard CLI state directory so auth/config come from ~/.codex.
34
+ */
35
+ export function buildCodeAgentSpawnEnv(base = process.env) {
36
+ const env = { ...base };
37
+ env.HOME ||= homedir();
38
+ env.CODEX_HOME = DEFAULT_CODEX_HOME;
39
+ delete env.CLAUDECODE;
40
+ delete env.GH_TOKEN;
41
+ delete env.GITHUB_TOKEN;
42
+ return env;
43
+ }
27
44
  /** Return supported coding CLIs currently available on PATH. */
28
45
  export function getAvailableCodingCliTools(commandChecker = isCommandAvailable) {
29
46
  const available = [];
@@ -31,19 +48,17 @@ export function getAvailableCodingCliTools(commandChecker = isCommandAvailable)
31
48
  available.push('codex');
32
49
  if (commandChecker('claude') || commandChecker('claude-code'))
33
50
  available.push('claude');
34
- if (commandChecker('kimi'))
35
- available.push('kimi');
36
51
  return available;
37
52
  }
38
53
  /** Return preflight error when no supported coding CLI is installed. */
39
54
  export function getCodingCliPreflightError(commandChecker = isCommandAvailable) {
40
55
  if (getAvailableCodingCliTools(commandChecker).length > 0)
41
56
  return null;
42
- return 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`), Claude Code CLI (`claude` or `claude-code`), or Kimi CLI (`kimi`).';
57
+ return 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`) or Claude Code CLI (`claude` or `claude-code`).';
43
58
  }
44
59
  /**
45
60
  * Normalize legacy/default agent values to supported CLI agent IDs.
46
- * Accepts strict IDs and older alias-like values (e.g. "claude-think").
61
+ * Accepts strict IDs and older alias-like values (e.g. "claude-coder").
47
62
  */
48
63
  export function normalizeCodeAgent(agent) {
49
64
  if (!agent)
@@ -53,16 +68,25 @@ export function normalizeCodeAgent(agent) {
53
68
  return 'claude';
54
69
  if (value === 'codex' || value.startsWith('codex'))
55
70
  return 'codex';
56
- if (value === 'kimi' || value.startsWith('kimi'))
57
- return 'kimi';
58
71
  return null;
59
72
  }
60
73
  /**
61
74
  * Resolve requested/default agent selection to a supported CLI agent ID.
62
75
  * Preference order: explicit request -> configured default -> "claude".
63
- * If no agent is explicit and the model is a GPT/OpenAI model, auto-select codex.
64
- * If the model is a kimi model, auto-select kimi.
76
+ * If no agent is explicit and the model is a GPT/Codex model, auto-select codex.
65
77
  */
78
+ /**
79
+ * Check if a model string is compatible with a given agent CLI.
80
+ * e.g. gpt-5.3-codex is NOT compatible with 'claude', claude-opus IS.
81
+ */
82
+ export function isModelCompatibleWithAgent(model, agent) {
83
+ const m = model.toLowerCase();
84
+ if (agent === 'codex') {
85
+ return /^(gpt|codex|o[134]|openai\/)/i.test(m);
86
+ }
87
+ // claude: compatible if NOT a known non-Claude model
88
+ return !/^(gpt|codex|o[134]|openai\/)/i.test(m);
89
+ }
66
90
  export function resolveSelectedCodeAgent(requestedAgent, defaultAgent, model) {
67
91
  // If no explicit agent was requested, infer from model
68
92
  if (!requestedAgent && model) {
@@ -70,8 +94,6 @@ export function resolveSelectedCodeAgent(requestedAgent, defaultAgent, model) {
70
94
  if (m.includes('gpt') || m.includes('codex') || m.startsWith('openai/') || m.startsWith('o1') || m.startsWith('o3') || m.startsWith('o4')) {
71
95
  return 'codex';
72
96
  }
73
- if (m.includes('kimi'))
74
- return 'kimi';
75
97
  }
76
98
  const candidate = requestedAgent || defaultAgent || 'claude';
77
99
  return normalizeCodeAgent(candidate);
@@ -85,6 +107,41 @@ export function setCodeAgentConfig(config) {
85
107
  export function getCodeAgentConfig() {
86
108
  return _codeAgentConfig;
87
109
  }
110
+ function stripProviderPrefix(model) {
111
+ return model.includes('/') ? model.split('/').slice(1).join('/') : model;
112
+ }
113
+ /** Read Claude Code's configured default model, if present. */
114
+ export function readClaudeCodeDefaultModel(settingsPath = join(homedir(), '.claude', 'settings.json')) {
115
+ try {
116
+ if (!existsSync(settingsPath))
117
+ return undefined;
118
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
119
+ const model = settings?.model;
120
+ return typeof model === 'string' && model.trim() ? model.trim() : undefined;
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ /** Resolve the model text shown in dashboards/reports without changing CLI args. */
127
+ export function resolveCodeAgentModelLabel(agent, model) {
128
+ if (model)
129
+ return stripProviderPrefix(model);
130
+ const normalizedAgent = normalizeCodeAgent(agent);
131
+ if (normalizedAgent === 'claude') {
132
+ const defaultModel = readClaudeCodeDefaultModel();
133
+ return defaultModel ? `${defaultModel} default` : 'claude default';
134
+ }
135
+ if (normalizedAgent === 'codex')
136
+ return 'codex default';
137
+ return `${agent || 'agent'} default`;
138
+ }
139
+ export function withCodeAgentModelLabel(task) {
140
+ return {
141
+ ...task,
142
+ modelLabel: task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model),
143
+ };
144
+ }
88
145
  /** Build CLI args for code_with_agent. Exported for testing. */
89
146
  export function buildCodeAgentArgs(input) {
90
147
  const agent = input.agent || 'claude';
@@ -95,56 +152,38 @@ export function buildCodeAgentArgs(input) {
95
152
  '--full-auto',
96
153
  '--json',
97
154
  '--color', 'never',
155
+ '--skip-git-repo-check',
98
156
  ];
99
157
  if (input.workdir)
100
158
  args.push('-C', input.workdir);
101
159
  if (input.model)
102
160
  args.push('-m', input.model);
161
+ if (input.effort)
162
+ args.push('-c', `model_reasoning_effort=${input.effort}`);
103
163
  args.push(input.task);
104
164
  return { cmd: CODEX_CLI_PATH, args };
105
165
  }
106
- if (agent === 'kimi') {
107
- const args = [
108
- '--yolo',
109
- '-p', input.task,
110
- ];
111
- if (input.workdir)
112
- args.push('-w', input.workdir);
113
- if (input.model)
114
- args.push('-m', input.model);
115
- return { cmd: KIMI_CLI_PATH, args };
116
- }
117
166
  // Default: claude
118
167
  // Each --allowedTools flag takes one tool name — repeat the flag per tool
119
168
  // --allowedTools restricts which tools are available (not just permissions).
120
- // Include Playwright MCP tools so the agent can use the browser.
121
- const allowedTools = ['Edit', 'Read', 'Write', 'Bash', 'Glob', 'Grep', 'mcp__playwright__*'];
169
+ const allowedTools = ['Edit', 'Read', 'Write', 'Bash', 'Glob', 'Grep'];
122
170
  const toolArgs = allowedTools.flatMap(t => ['--allowedTools', t]);
123
- // Pass Playwright MCP server so coding agents share SkimpyClaw's browser profile
124
- // Must use chromium (not chrome) to match SkimpyClaw's browser-tool.ts persistent context
125
- const playwrightMcp = JSON.stringify({
126
- mcpServers: {
127
- playwright: {
128
- command: 'npx',
129
- args: ['-y', '@playwright/mcp@latest', '--browser', 'chromium',
130
- '--user-data-dir', join(homedir(), '.skimpyclaw', 'browser-profile'),
131
- '--caps', 'vision'],
132
- },
133
- },
134
- });
135
171
  const args = [
136
172
  '-p',
137
173
  '--verbose',
138
174
  '--output-format', 'stream-json',
139
175
  '--dangerously-skip-permissions',
140
- '--mcp-config', playwrightMcp,
141
176
  ...toolArgs,
142
177
  '--max-turns', maxTurns,
143
178
  '--append-system-prompt', `Output text only. Never use say or TTS. Focus on the coding task. Run ${buildValidationCommand(input.workdir || process.cwd())} to verify changes.`,
144
179
  ];
180
+ // Interactive mode: pin this turn to a known session UUID so follow-ups can --resume it.
181
+ if (input.sessionId) {
182
+ args.push('--session-id', input.sessionId);
183
+ }
145
184
  // Only pass model to Claude CLI if it's not a known non-Claude model.
146
- // GPT/Codex/Kimi/o-series models would be rejected by the Claude CLI.
147
- if (input.model && !/^(gpt|codex|kimi|o[134]|openai\/)/i.test(input.model)) {
185
+ // GPT/Codex/o-series models would be rejected by the Claude CLI.
186
+ if (input.model && !/^(gpt|codex|o[134]|openai\/)/i.test(input.model)) {
148
187
  args.push('--model', input.model);
149
188
  }
150
189
  args.push(input.task);
@@ -156,42 +195,6 @@ export function formatDuration(seconds) {
156
195
  return '?';
157
196
  return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
158
197
  }
159
- /** Build notification for a team-coordinator task with child results. */
160
- export function buildTeamNotification(task, getChildTask) {
161
- const dur = formatDuration(task.durationSeconds);
162
- const taskPreview = task.task.length > 100 ? task.task.slice(0, 100) + '...' : task.task;
163
- const statusIcon = task.status === 'completed' ? '✅' : task.status === 'timeout' ? '⏰' : '❌';
164
- const validation = task.validationPassed ? ' Tests pass.' : '';
165
- const lines = [];
166
- lines.push(`${statusIcon} Team ${task.id} ${task.status} (${dur}).${validation}`);
167
- lines.push(`Task: ${taskPreview}`);
168
- // Per-child summary
169
- const childIds = task.childTaskIds || [];
170
- if (childIds.length > 0) {
171
- lines.push('');
172
- for (const childId of childIds) {
173
- const child = getChildTask(childId);
174
- if (!child)
175
- continue;
176
- const childIcon = child.status === 'completed' ? '✅' : child.status === 'failed' ? '❌' : child.status === 'timeout' ? '⏰' : '❓';
177
- const childDur = formatDuration(child.durationSeconds);
178
- const subtask = (child.subtask || child.task || '').slice(0, 80);
179
- lines.push(` ${childIcon} ${child.id} (${childDur}): ${subtask}`);
180
- }
181
- }
182
- // Synthesis result
183
- if (task.outputPreview) {
184
- lines.push(`\nResult: ${task.outputPreview}`);
185
- }
186
- // Errors
187
- if (task.error && task.error !== 'Validation failed') {
188
- lines.push(`\nError: ${task.error}`);
189
- }
190
- if (task.validationOutput) {
191
- lines.push(`\nValidation:\n${task.validationOutput.slice(0, 800)}`);
192
- }
193
- return lines.join('\n');
194
- }
195
198
  /** Build notification for a single code agent. */
196
199
  export function buildSoloNotification(task) {
197
200
  const dur = formatDuration(task.durationSeconds);
@@ -218,30 +221,282 @@ export function buildSoloNotification(task) {
218
221
  return message;
219
222
  }
220
223
  }
224
+ function shorten(value, maxChars) {
225
+ const trimmed = value.trim();
226
+ if (trimmed.length <= maxChars)
227
+ return trimmed;
228
+ return `${trimmed.slice(0, maxChars - 3).trim()}...`;
229
+ }
230
+ function normalizeOutputPaths(value, workdir) {
231
+ if (!value)
232
+ return value;
233
+ const normalizedWorkdir = workdir.replace(/\/+$/, '');
234
+ if (!normalizedWorkdir)
235
+ return value;
236
+ return value
237
+ .split(`${normalizedWorkdir}/`).join('')
238
+ .split(normalizedWorkdir).join('.');
239
+ }
240
+ function extractStreamJsonText(line) {
241
+ let event;
242
+ try {
243
+ event = JSON.parse(line);
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ if (!event || typeof event !== 'object' || typeof event.type !== 'string') {
249
+ return null;
250
+ }
251
+ if ((event.type === 'output_text' || event.output_text) && (event.output_text || event.text)) {
252
+ return String(event.output_text || event.text).trim() || null;
253
+ }
254
+ if (event.type === 'result' && typeof event.result === 'string') {
255
+ return event.result.trim() || null;
256
+ }
257
+ if ((event.type === 'item.completed' || event.type === 'item.delta') && event.item?.type === 'agent_message' && event.item?.text) {
258
+ return String(event.item.text).trim() || null;
259
+ }
260
+ return '';
261
+ }
262
+ function looksLikeRawStreamJson(line) {
263
+ const trimmed = line.trim();
264
+ return trimmed.startsWith('{') && /"type"\s*:|"item"\s*:|"thread_id"\s*:/.test(trimmed);
265
+ }
266
+ function stripStreamJsonNoise(value) {
267
+ const lines = value.split('\n');
268
+ const cleaned = [];
269
+ let sawStreamJson = false;
270
+ for (const line of lines) {
271
+ const extracted = extractStreamJsonText(line);
272
+ if (extracted !== null) {
273
+ sawStreamJson = true;
274
+ if (extracted)
275
+ cleaned.push(extracted);
276
+ continue;
277
+ }
278
+ if (looksLikeRawStreamJson(line)) {
279
+ sawStreamJson = true;
280
+ continue;
281
+ }
282
+ cleaned.push(line);
283
+ }
284
+ const result = cleaned.join('\n').trim();
285
+ return sawStreamJson ? result : value.trim();
286
+ }
287
+ function normalizeHeading(line) {
288
+ const normalized = line
289
+ .trim()
290
+ .replace(/^#{1,6}\s*/, '')
291
+ .replace(/^\*\*/, '')
292
+ .replace(/\*\*$/, '')
293
+ .replace(/:$/, '')
294
+ .trim()
295
+ .toLowerCase();
296
+ const allowed = new Set(['decision', 'summary', 'findings', 'risk checks', 'tests', 'recommendation']);
297
+ return allowed.has(normalized) ? normalized : null;
298
+ }
299
+ function extractReportSections(value) {
300
+ const sections = {};
301
+ let current = 'summary';
302
+ for (const line of value.split('\n')) {
303
+ const heading = normalizeHeading(line);
304
+ if (heading) {
305
+ current = heading;
306
+ sections[current] ||= [];
307
+ continue;
308
+ }
309
+ sections[current] ||= [];
310
+ sections[current].push(line);
311
+ }
312
+ return Object.fromEntries(Object.entries(sections)
313
+ .map(([key, lines]) => [key, lines.join('\n').trim()])
314
+ .filter(([, text]) => text));
315
+ }
316
+ function firstMeaningfulLines(value, maxChars) {
317
+ const lines = value
318
+ .split('\n')
319
+ .map(line => line.trim())
320
+ .filter(Boolean)
321
+ .filter(line => !looksLikeRawStreamJson(line))
322
+ .filter(line => !/^i('|’)ll\b/i.test(line))
323
+ .filter(line => !/^i am\b/i.test(line));
324
+ return shorten(lines.join('\n'), maxChars);
325
+ }
326
+ function formatAgentDisplay(task) {
327
+ const agent = task.agent === 'team-coordinator' ? 'TEAM' : task.agent.toUpperCase();
328
+ const model = task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model);
329
+ const effort = task.effort ? ` · effort ${task.effort}` : '';
330
+ return `${agent} · ${model}${effort}`;
331
+ }
332
+ function buildReportMarkdown(task, result) {
333
+ const validation = task.validationPassed === true
334
+ ? 'pass'
335
+ : task.validationPassed === false
336
+ ? 'fail'
337
+ : 'not run';
338
+ return [
339
+ `# Code Agent ${task.id}`,
340
+ '',
341
+ `- Status: ${task.status}`,
342
+ `- Agent: ${task.agent}`,
343
+ `- Model: ${task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model)}`,
344
+ `- Effort: ${task.effort || 'default'}`,
345
+ `- Duration: ${formatDuration(task.durationSeconds)}`,
346
+ `- Validation: ${validation}`,
347
+ `- Workdir: ${task.workdir}`,
348
+ task.sourceWorkdir ? `- Source Workdir: ${task.sourceWorkdir}` : '',
349
+ task.worktreePath ? `- Worktree: ${task.worktreePath}` : '',
350
+ task.worktreeCleanup ? `- Worktree Cleanup: ${task.worktreeCleanup.status}${task.worktreeCleanup.reason ? ` (${task.worktreeCleanup.reason})` : ''}` : '',
351
+ '',
352
+ '## Task',
353
+ '',
354
+ task.task,
355
+ '',
356
+ '## Result',
357
+ '',
358
+ result || task.error || '(No result captured.)',
359
+ task.validationOutput ? `\n## Validation Output\n\n${task.validationOutput}` : '',
360
+ ].filter(Boolean).join('\n');
361
+ }
362
+ function buildDiscordResultSummary(task, result) {
363
+ if (!result.trim())
364
+ return task.error ? `Error: ${shorten(task.error, 700)}` : '';
365
+ const sections = extractReportSections(result);
366
+ const lines = [];
367
+ const decision = sections.decision || sections.recommendation;
368
+ if (decision) {
369
+ lines.push(`**Decision**\n${shorten(firstMeaningfulLines(decision, 450), 450)}`);
370
+ }
371
+ if (sections.findings) {
372
+ lines.push(`**Findings**\n${shorten(firstMeaningfulLines(sections.findings, 650), 650)}`);
373
+ }
374
+ if (sections['risk checks']) {
375
+ lines.push(`**Risk Checks**\n${shorten(firstMeaningfulLines(sections['risk checks'], 420), 420)}`);
376
+ }
377
+ if (sections.tests) {
378
+ lines.push(`**Tests**\n${shorten(firstMeaningfulLines(sections.tests, 240), 240)}`);
379
+ }
380
+ if (lines.length > 0)
381
+ return lines.join('\n\n');
382
+ return shorten(firstMeaningfulLines(result, 950), 950);
383
+ }
384
+ export function buildCodeAgentDiscordNotification(task) {
385
+ const dur = formatDuration(task.durationSeconds);
386
+ const icon = task.status === 'completed' ? '✅' : task.status === 'timeout' ? '⏰' : '❌';
387
+ const status = task.status === 'completed'
388
+ ? 'completed'
389
+ : task.status === 'timeout'
390
+ ? 'timed out'
391
+ : 'failed';
392
+ const validation = task.validationPassed === true
393
+ ? 'Build/tests pass'
394
+ : task.validationPassed === false
395
+ ? 'Build/tests failed'
396
+ : task.status === 'completed'
397
+ ? 'Validation not run'
398
+ : undefined;
399
+ const rawResult = normalizeOutputPaths(stripStreamJsonNoise(task.outputPreview || task.liveOutput || task.validationOutput || task.error || ''), task.workdir);
400
+ const taskPreview = normalizeOutputPaths(shorten(task.task, 350), task.workdir);
401
+ const summary = buildDiscordResultSummary(task, rawResult);
402
+ const contentParts = [
403
+ `${icon} \`${task.id}\` ${status} · ${formatAgentDisplay(task)} · ${dur}`,
404
+ validation ? `**Validation:** ${validation}` : undefined,
405
+ task.retryCount ? `**Retries:** ${task.retryCount}` : undefined,
406
+ `**Task:** ${taskPreview}`,
407
+ summary ? `\n${summary}` : undefined,
408
+ ].filter(Boolean);
409
+ const needsAttachment = rawResult.length > 1_200 || task.validationOutput || task.task.length > 350;
410
+ const attachment = needsAttachment
411
+ ? {
412
+ name: `${task.id}-report.md`,
413
+ description: `Full report for ${task.id}`,
414
+ content: buildReportMarkdown(task, rawResult),
415
+ }
416
+ : undefined;
417
+ const content = [
418
+ contentParts.join('\n'),
419
+ attachment ? '\nFull report attached.' : undefined,
420
+ ].filter(Boolean).join('\n');
421
+ return { content, attachment };
422
+ }
423
+ function resolveDiscordThreadId(task) {
424
+ return task.discordThreadId;
425
+ }
426
+ function resolveDiscordChannelId(task) {
427
+ return task.discordChannelId;
428
+ }
429
+ /**
430
+ * Try to send a notification to a Discord thread associated with this task.
431
+ * Returns true if successfully sent to thread.
432
+ */
433
+ async function trySendToDiscordThread(task, notification) {
434
+ const threadId = resolveDiscordThreadId(task);
435
+ if (!threadId)
436
+ return false;
437
+ try {
438
+ const { sendToDiscordThread, sendToDiscordThreadWithAttachments } = await import('../channels/discord/index.js');
439
+ const sent = notification.attachment
440
+ ? await sendToDiscordThreadWithAttachments(threadId, notification.content, [notification.attachment])
441
+ : await sendToDiscordThread(threadId, notification.content);
442
+ if (sent) {
443
+ console.log(`[code-agent] Notification for ${task.id} sent to thread ${threadId}`);
444
+ }
445
+ return sent;
446
+ }
447
+ catch (err) {
448
+ console.error(`[code-agent] Failed to send to Discord thread for ${task.id}:`, err);
449
+ return false;
450
+ }
451
+ }
452
+ /**
453
+ * Try to send a notification to the originating Discord channel for this task.
454
+ * Returns true if successfully sent to channel.
455
+ */
456
+ async function trySendToDiscordChannel(task, notification) {
457
+ const channelId = resolveDiscordChannelId(task);
458
+ if (!channelId)
459
+ return false;
460
+ try {
461
+ const { sendDiscordProactiveMessage, sendDiscordProactiveMessageWithAttachments } = await import('../channels/discord/index.js');
462
+ if (notification.attachment) {
463
+ await sendDiscordProactiveMessageWithAttachments(channelId, notification.content, [notification.attachment]);
464
+ }
465
+ else {
466
+ await sendDiscordProactiveMessage(channelId, notification.content);
467
+ }
468
+ console.log(`[code-agent] Notification for ${task.id} sent to Discord channel ${channelId}`);
469
+ return true;
470
+ }
471
+ catch (err) {
472
+ console.error(`[code-agent] Failed to send to Discord channel for ${task.id}:`, err);
473
+ return false;
474
+ }
475
+ }
476
+ function hasDiscordRouting(task) {
477
+ return !!(resolveDiscordThreadId(task) || resolveDiscordChannelId(task));
478
+ }
221
479
  /** Send auto-notification to active channel on completion/failure. */
222
- export async function notifyCodeAgentResult(task, getChildTask) {
480
+ export async function notifyCodeAgentResult(task) {
223
481
  if (!_codeAgentConfig)
224
482
  return;
225
483
  const { sendActiveChannelProactiveMessage } = await import('../channels.js');
226
- let message;
227
- // Team coordinator gets a structured notification
228
- if (task.agent === 'team-coordinator') {
229
- message = buildTeamNotification(task, getChildTask);
484
+ const message = buildSoloNotification(task);
485
+ const discordNotification = buildCodeAgentDiscordNotification(task);
486
+ // Prefer task-scoped Discord routing over global active-channel fallback.
487
+ const threadSent = await trySendToDiscordThread(task, discordNotification);
488
+ const channelSent = threadSent ? true : await trySendToDiscordChannel(task, discordNotification);
489
+ if (!threadSent && !channelSent && !hasDiscordRouting(task)) {
230
490
  const sent = await sendActiveChannelProactiveMessage(_codeAgentConfig, message).catch((err) => {
231
- console.error(`[code-agent] Failed to send team notification for ${task.id}:`, err);
491
+ console.error(`[code-agent] Failed to send notification for ${task.id}:`, err);
232
492
  return false;
233
493
  });
234
494
  if (!sent)
235
- console.warn(`[code-agent] Team notification not delivered for ${task.id} (no active channel or target)`);
236
- return;
495
+ console.warn(`[code-agent] Notification not delivered for ${task.id} (no active channel or target)`);
496
+ }
497
+ else if (!threadSent && !channelSent) {
498
+ console.warn(`[code-agent] Notification not delivered for ${task.id} (Discord thread/channel unavailable)`);
237
499
  }
238
- message = buildSoloNotification(task);
239
- const sent = await sendActiveChannelProactiveMessage(_codeAgentConfig, message).catch((err) => {
240
- console.error(`[code-agent] Failed to send notification for ${task.id}:`, err);
241
- return false;
242
- });
243
- if (!sent)
244
- console.warn(`[code-agent] Notification not delivered for ${task.id} (no active channel or target)`);
245
500
  }
246
501
  /** Check workdir against allowed paths. */
247
502
  export function resolveWorkdir(rawWorkdir, projects, skimpyclawRoot) {
@@ -268,9 +523,8 @@ export function resolveModelAlias(model, aliases) {
268
523
  if (/^claude[-.]3[-.]5[-.]haiku(?:[-_.].*)?$/i.test(model)) {
269
524
  return 'claude-haiku-4-5';
270
525
  }
526
+ if (/^claude[-.]opus[-.]4[-_.]6$/i.test(model)) {
527
+ return 'claude-opus-4-6';
528
+ }
271
529
  return model;
272
530
  }
273
- /** @deprecated Removed — old Claude CLI team state reader. Kept for backward compat. */
274
- export function readTeamState() {
275
- return null;
276
- }
@@ -0,0 +1,37 @@
1
+ export type CodeAgentWorktreeMode = 'off' | 'auto' | 'always';
2
+ export type CodeAgentWorktreeRequest = boolean | 'auto' | undefined;
3
+ export interface CodeAgentWorktreeConfig {
4
+ enabled?: boolean;
5
+ mode?: CodeAgentWorktreeMode;
6
+ root?: string;
7
+ cleanup?: boolean;
8
+ }
9
+ export interface PreparedCodeAgentWorktree {
10
+ sourceWorkdir: string;
11
+ worktreePath: string;
12
+ runWorkdir: string;
13
+ worktreeRef: string;
14
+ gitRoot: string;
15
+ }
16
+ export interface CodeAgentWorktreeCleanupResult {
17
+ status: 'removed' | 'preserved' | 'skipped' | 'failed';
18
+ path?: string;
19
+ reason?: string;
20
+ at: string;
21
+ }
22
+ export declare function normalizeWorktreeRequest(value: unknown): CodeAgentWorktreeRequest;
23
+ export declare function shouldAutoWorktreeTask(task: string): boolean;
24
+ export declare function shouldUseCodeAgentWorktree(task: string, request: CodeAgentWorktreeRequest, config?: CodeAgentWorktreeConfig): boolean;
25
+ export declare function findGitRoot(workdir: string): string | null;
26
+ export declare function prepareCodeAgentWorktree(options: {
27
+ id: string;
28
+ sourceWorkdir: string;
29
+ config?: CodeAgentWorktreeConfig;
30
+ required?: boolean;
31
+ }): PreparedCodeAgentWorktree | undefined;
32
+ export declare function cleanupCodeAgentWorktree(options: {
33
+ sourceWorkdir?: string;
34
+ worktreePath?: string;
35
+ worktreeRef?: string;
36
+ config?: CodeAgentWorktreeConfig;
37
+ }): CodeAgentWorktreeCleanupResult;