@gajae-code/coding-agent 0.7.9 โ†’ 0.7.10

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 (53) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -0
  3. package/dist/types/config/settings-schema.d.ts +10 -1
  4. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +7 -0
  6. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
  7. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  8. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  9. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
  10. package/dist/types/notifications/html-format.d.ts +2 -0
  11. package/dist/types/notifications/recent-activity.d.ts +5 -1
  12. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  13. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  14. package/dist/types/notifications/threaded-render.d.ts +1 -0
  15. package/dist/types/tools/ask.d.ts +2 -0
  16. package/dist/types/utils/cmux-workspace.d.ts +23 -0
  17. package/dist/types/web/search/index.d.ts +1 -1
  18. package/dist/types/web/search/provider.d.ts +5 -0
  19. package/dist/types/web/search/render.d.ts +1 -0
  20. package/package.json +7 -7
  21. package/src/cli/web-search-cli.ts +3 -2
  22. package/src/config/settings-schema.ts +11 -1
  23. package/src/coordinator-mcp/server.ts +1 -1
  24. package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
  25. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  26. package/src/extensibility/extensions/types.ts +11 -0
  27. package/src/gjc-runtime/launch-tmux.ts +63 -2
  28. package/src/gjc-runtime/ultragoal-guard.ts +139 -19
  29. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  30. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  31. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  32. package/src/hooks/skill-state.ts +33 -73
  33. package/src/internal-urls/docs-index.generated.ts +2 -1
  34. package/src/modes/components/hook-selector.ts +21 -3
  35. package/src/modes/components/settings-selector.ts +47 -3
  36. package/src/modes/components/status-line/presets.ts +11 -0
  37. package/src/modes/controllers/extension-ui-controller.ts +18 -2
  38. package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
  39. package/src/notifications/html-format.ts +75 -1
  40. package/src/notifications/index.ts +12 -1
  41. package/src/notifications/recent-activity.ts +32 -8
  42. package/src/notifications/telegram-daemon-control.ts +9 -0
  43. package/src/notifications/telegram-daemon.ts +67 -24
  44. package/src/notifications/telegram-reference.ts +36 -14
  45. package/src/notifications/threaded-render.ts +14 -6
  46. package/src/prompts/system/system-prompt.md +5 -1
  47. package/src/sdk.ts +3 -5
  48. package/src/tools/ask.ts +136 -16
  49. package/src/utils/cmux-workspace.ts +106 -0
  50. package/src/utils/title-generator.ts +2 -0
  51. package/src/web/search/index.ts +22 -10
  52. package/src/web/search/provider.ts +15 -0
  53. package/src/web/search/render.ts +3 -0
@@ -11,7 +11,13 @@ import { resolveGjcRuntimeSpawnInfo } from "../daemon/runtime";
11
11
  import { getNotificationConfig, isGloballyConfigured, tokenFingerprint } from "./config";
12
12
  import { parseInThreadConfigCommand } from "./config-commands";
13
13
  import { daemonPaths } from "./daemon-paths";
14
- import { buildCompactChoiceGrid, TELEGRAM_PARSE_MODE } from "./html-format";
14
+ import {
15
+ buildCompactChoiceGrid,
16
+ pre,
17
+ splitTelegramHtml,
18
+ TELEGRAM_MESSAGE_LIMIT,
19
+ TELEGRAM_PARSE_MODE,
20
+ } from "./html-format";
15
21
  import type {
16
22
  SessionCloseTarget,
17
23
  SessionCreateTarget,
@@ -122,6 +128,21 @@ const QUEUED_REACTION = "๐Ÿ‘€";
122
128
  const PENDING_TOPIC_FRAME_LIMIT = 20;
123
129
  const CONSUMED_REACTION = "โœ…";
124
130
 
131
+ function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
132
+ if (text.length <= max) return [text];
133
+ const chunks: string[] = [];
134
+ let out = "";
135
+ for (const ch of text) {
136
+ if (out.length + ch.length > max) {
137
+ chunks.push(out);
138
+ out = "";
139
+ }
140
+ out += ch;
141
+ }
142
+ if (out) chunks.push(out);
143
+ return chunks;
144
+ }
145
+
125
146
  /**
126
147
  * Whether `err` is a transient network failure worth retrying. Telegram API
127
148
  * calls over HTTP/2 occasionally surface mid-stream `ECONNRESET` (and similar)
@@ -1053,14 +1074,29 @@ export class TelegramNotificationDaemon {
1053
1074
  threadId: number | undefined,
1054
1075
  ): Promise<boolean> {
1055
1076
  if (!isLifecycleCommandText(text)) return false;
1056
- const reply = (body: string) =>
1057
- this.botApi
1058
- .call("sendMessage", {
1059
- chat_id: this.opts.chatId,
1060
- ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
1061
- text: body,
1062
- })
1063
- .catch(() => undefined);
1077
+ const reply = async (body: string): Promise<void> => {
1078
+ for (const text of splitTelegramPlainText(body)) {
1079
+ await this.botApi
1080
+ .call("sendMessage", {
1081
+ chat_id: this.opts.chatId,
1082
+ ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
1083
+ text,
1084
+ })
1085
+ .catch(() => undefined);
1086
+ }
1087
+ };
1088
+ const replyHtml = async (body: string): Promise<void> => {
1089
+ for (const text of splitTelegramHtml(body)) {
1090
+ await this.botApi
1091
+ .call("sendMessage", {
1092
+ chat_id: this.opts.chatId,
1093
+ ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
1094
+ text,
1095
+ parse_mode: TELEGRAM_PARSE_MODE,
1096
+ })
1097
+ .catch(() => undefined);
1098
+ }
1099
+ };
1064
1100
 
1065
1101
  if (!this.lifecycleControlActive) {
1066
1102
  await reply("Session lifecycle control is not available right now.");
@@ -1079,11 +1115,12 @@ export class TelegramNotificationDaemon {
1079
1115
  const recent = listRecentSessions({
1080
1116
  sessionsRoot: path.join(this.opts.settings.getAgentDir(), "sessions"),
1081
1117
  limit: 10,
1118
+ includeInternal: false,
1082
1119
  });
1083
1120
  const lines = recent.length
1084
- ? recent.map(e => `\u2022 ${e.sessionId}${e.path ? ` (${e.path})` : ""}`).join("\n")
1121
+ ? recent.map(e => `โ€ข ${e.sessionId}${e.path ? ` (${e.path})` : ""}`).join("\n")
1085
1122
  : "No recent sessions.";
1086
- await reply(lines);
1123
+ await replyHtml(pre(lines));
1087
1124
  return true;
1088
1125
  }
1089
1126
 
@@ -1612,12 +1649,14 @@ export class TelegramNotificationDaemon {
1612
1649
  parse_mode: TELEGRAM_PARSE_MODE,
1613
1650
  });
1614
1651
  } else if (send.text) {
1615
- await this.botApi.call("sendMessage", {
1616
- chat_id: this.opts.chatId,
1617
- ...threadField,
1618
- text: send.text,
1619
- parse_mode: TELEGRAM_PARSE_MODE,
1620
- });
1652
+ for (const text of splitTelegramHtml(send.text)) {
1653
+ await this.botApi.call("sendMessage", {
1654
+ chat_id: this.opts.chatId,
1655
+ ...threadField,
1656
+ text,
1657
+ parse_mode: TELEGRAM_PARSE_MODE,
1658
+ });
1659
+ }
1621
1660
  }
1622
1661
  } catch {
1623
1662
  // Best-effort: a failed send must never stop the daemon.
@@ -1820,13 +1859,17 @@ export class TelegramNotificationDaemon {
1820
1859
  const inline_keyboard = buildCompactChoiceGrid(options, (i: number) =>
1821
1860
  this.aliasTable.put({ sessionId: session.sessionId, actionId: msg.id, answer: i }),
1822
1861
  );
1823
- const result = (await this.botApi.call("sendMessage", {
1824
- chat_id: this.opts.chatId,
1825
- ...threadField,
1826
- text: rendered.text,
1827
- parse_mode: TELEGRAM_PARSE_MODE,
1828
- ...(inline_keyboard.length ? { reply_markup: { inline_keyboard } } : {}),
1829
- })) as { result?: { message_id?: number } };
1862
+ const chunks = splitTelegramHtml(rendered.text);
1863
+ let result: { result?: { message_id?: number } } = {};
1864
+ for (let i = 0; i < chunks.length; i++) {
1865
+ result = (await this.botApi.call("sendMessage", {
1866
+ chat_id: this.opts.chatId,
1867
+ ...threadField,
1868
+ text: chunks[i]!,
1869
+ parse_mode: TELEGRAM_PARSE_MODE,
1870
+ ...(i === chunks.length - 1 && inline_keyboard.length ? { reply_markup: { inline_keyboard } } : {}),
1871
+ })) as { result?: { message_id?: number } };
1872
+ }
1830
1873
  const messageId = result.result?.message_id;
1831
1874
  if (messageId !== undefined)
1832
1875
  this.messageRoutes.set(String(messageId), { sessionId: session.sessionId, actionId: msg.id });
@@ -20,8 +20,8 @@ import {
20
20
  buildCompactChoiceGrid,
21
21
  escapeHtml,
22
22
  numberedOptionList,
23
+ splitTelegramHtml,
23
24
  TELEGRAM_PARSE_MODE,
24
- truncateTelegramHtml,
25
25
  } from "./html-format";
26
26
  import { renderThreadedFrame } from "./threaded-render";
27
27
 
@@ -37,6 +37,8 @@ export interface RenderedMessage {
37
37
  inline_keyboard?: InlineButton[][];
38
38
  }
39
39
 
40
+ type TelegramSend = (method: string, body: unknown) => Promise<Response>;
41
+
40
42
  /** Encode `actionId` + option `index` into Telegram callback_data (<=64 bytes). */
41
43
  export function encodeCallbackData(actionId: string, index: number): string {
42
44
  return `r:${index}:${actionId}`.slice(0, 64);
@@ -131,14 +133,32 @@ export function buildActionMessage(action: {
131
133
  }): RenderedMessage {
132
134
  if (action.kind === "idle") {
133
135
  const text = action.summary ? `๐ŸŸข Agent idle\n${escapeHtml(action.summary)}` : "๐ŸŸข Agent idle";
134
- return { text: truncateTelegramHtml(text) };
136
+ return { text };
135
137
  }
136
138
  const text = `โ“ ${bold(action.question ?? "Question")}`;
137
139
  const options = action.options ?? [];
138
- if (options.length === 0) return { text: truncateTelegramHtml(`${text}\n\n(reply with text)`) };
140
+ if (options.length === 0) return { text: `${text}\n\n(reply with text)` };
139
141
  const body = `${text}\n\n${numberedOptionList(options)}`;
140
142
  const inline_keyboard = buildCompactChoiceGrid(options, i => encodeCallbackData(action.id, i));
141
- return { text: truncateTelegramHtml(body), inline_keyboard };
143
+ return { text: body, inline_keyboard };
144
+ }
145
+
146
+ /** Send Telegram HTML text chunks sequentially so long messages preserve order. */
147
+ export async function sendTelegramHtmlChunks(
148
+ send: TelegramSend,
149
+ chatId: string,
150
+ text: string,
151
+ inlineKeyboard?: InlineButton[][],
152
+ ): Promise<void> {
153
+ const chunks = splitTelegramHtml(text);
154
+ for (let i = 0; i < chunks.length; i++) {
155
+ await send("sendMessage", {
156
+ chat_id: chatId,
157
+ text: chunks[i]!,
158
+ parse_mode: TELEGRAM_PARSE_MODE,
159
+ ...(i === chunks.length - 1 && inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
160
+ });
161
+ }
142
162
  }
143
163
 
144
164
  /** A protocol `reply` frame the client should send to the server. */
@@ -285,15 +305,15 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
285
305
  const ws = new WebSocket(`${url}/?token=${encodeURIComponent(token)}`);
286
306
  let latestPendingAskId: string | undefined;
287
307
 
288
- const send = (method: string, body: unknown): Promise<Response> =>
308
+ const send: TelegramSend = (method, body) =>
289
309
  fetchImpl(`${api}/${method}`, {
290
310
  method: "POST",
291
311
  headers: { "content-type": "application/json" },
292
312
  body: JSON.stringify(body),
293
313
  });
294
314
 
295
- ws.addEventListener("message", (ev: MessageEvent) => {
296
- const msg = JSON.parse(String(ev.data)) as {
315
+ const handleServerMessage = async (data: string): Promise<void> => {
316
+ const msg = JSON.parse(data) as {
297
317
  type: string;
298
318
  kind?: "ask" | "idle";
299
319
  id?: string;
@@ -311,12 +331,7 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
311
331
  options: msg.options,
312
332
  summary: msg.summary,
313
333
  });
314
- void send("sendMessage", {
315
- chat_id: opts.chatId,
316
- text: rendered.text,
317
- parse_mode: TELEGRAM_PARSE_MODE,
318
- ...(rendered.inline_keyboard ? { reply_markup: { inline_keyboard: rendered.inline_keyboard } } : {}),
319
- });
334
+ await sendTelegramHtmlChunks(send, opts.chatId, rendered.text, rendered.inline_keyboard);
320
335
  } else if (msg.type === "action_resolved" && msg.id === latestPendingAskId) {
321
336
  latestPendingAskId = undefined;
322
337
  } else {
@@ -325,9 +340,16 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
325
340
  // session's forum topic; this reference shows the minimal handling.
326
341
  const threaded = renderThreadedFrame(msg as never);
327
342
  if (threaded?.text) {
328
- void send("sendMessage", { chat_id: opts.chatId, text: threaded.text, parse_mode: TELEGRAM_PARSE_MODE });
343
+ await sendTelegramHtmlChunks(send, opts.chatId, threaded.text);
329
344
  }
330
345
  }
346
+ };
347
+
348
+ let messageQueue = Promise.resolve();
349
+ ws.addEventListener("message", ev => {
350
+ const data = String(ev.data);
351
+ messageQueue = messageQueue.catch(() => undefined).then(() => handleServerMessage(data));
352
+ void messageQueue.catch(() => undefined);
331
353
  });
332
354
 
333
355
  // Telegram long-poll loop.
@@ -49,6 +49,7 @@ interface ThreadedFrame {
49
49
  tokenUsage?: unknown;
50
50
  model?: unknown;
51
51
  diff?: unknown;
52
+ cwd?: unknown;
52
53
  // turn_stream
53
54
  phase?: unknown;
54
55
  text?: unknown;
@@ -89,18 +90,25 @@ export function formatIdentityHeader(frame: {
89
90
  /** Format a streamed context update into a compact block (omitting empty fields). */
90
91
  export function formatContextUpdate(frame: ThreadedFrame): string | undefined {
91
92
  const lines: string[] = [];
93
+ const session = str(frame.sessionId);
94
+ const cwd = str(frame.cwd);
92
95
  const last = str(frame.lastMessage);
93
- if (last) lines.push(escapeHtml(truncate(last, 600)));
94
96
  const task = str(frame.task);
95
- if (task) lines.push(`${italic("task:")} ${escapeHtml(task)}`);
96
97
  const goal = str(frame.goal);
97
- if (goal) lines.push(`${italic("goal:")} ${escapeHtml(goal)}`);
98
98
  const usage = str(frame.tokenUsage);
99
99
  const model = str(frame.model);
100
- if (usage || model) lines.push(`ctx: ${code([usage, model].filter(Boolean).join(" ยท "))}`);
101
100
  const diff = str(frame.diff);
101
+ if (!cwd && !last && !task && !goal && !usage && !model && !diff) return undefined;
102
+ const meta = [session ? `session: ${code(session)}` : undefined, cwd ? `cwd: ${code(cwd)}` : undefined].filter(
103
+ Boolean,
104
+ );
105
+ if (meta.length > 0) lines.push(meta.join(" ยท "));
106
+ if (last) lines.push(escapeHtml(truncate(last, 600)));
107
+ if (task) lines.push(`${italic("task:")} ${escapeHtml(task)}`);
108
+ if (goal) lines.push(`${italic("goal:")} ${escapeHtml(goal)}`);
109
+ if (usage || model) lines.push(`ctx: ${code([usage, model].filter(Boolean).join(" ยท "))}`);
102
110
  if (diff) lines.push(`diff:\n${pre(truncate(diff, 1200))}`);
103
- return lines.length ? lines.join("\n") : undefined;
111
+ return lines.join("\n");
104
112
  }
105
113
 
106
114
  /**
@@ -125,7 +133,7 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
125
133
  case "turn_stream": {
126
134
  const raw = str(frame.text);
127
135
  if (!raw) return undefined;
128
- const text = finalizeTelegramHtml(markdownToTelegramHtml(raw));
136
+ const text = markdownToTelegramHtml(raw);
129
137
  const finalized = frame.phase === "finalized";
130
138
  return {
131
139
  method: "sendMessage",
@@ -60,6 +60,9 @@ Use for read-only plan critique. It approves only when execution can proceed wit
60
60
 
61
61
  <routing>
62
62
  - Clear, low-risk implementation request โ†’ implement directly with focused verification.
63
+ - Do not invoke `deep-interview`, `ralplan`, `ultragoal`, `team`, or role agents for simple clear implementation requests; direct tools and the default launch path are enough.
64
+ - When a task is clear, bounded, and low-risk, the default action is to make the smallest correct change and verify it, not to interview, plan, open a durable ledger, or delegate.
65
+ - Small verification needs do not make a task a planning workflow. Escalate only for real ambiguity, non-trivial architecture/sequence risk, durable multi-goal tracking, or useful coordinated workers.
63
66
  - Vague requirements โ†’ use `deep-interview` before planning or execution.
64
67
  - Clear requirements but non-trivial architecture/sequence risk โ†’ use `ralplan` and stop at pending approval.
65
68
  - Durable goal ledger needed โ†’ use `ultragoal`; if no approved plan exists, run `ralplan` first.
@@ -238,9 +241,10 @@ For image understanding, call `{{toolRefs.read}}` on the image path; the image i
238
241
  </before-editing>
239
242
 
240
243
  <decomposition>
241
- - Use todo tracking for tasks with three or more distinct steps.
244
+ - Use todo tracking for tasks with three or more distinct steps; skip it for one-step or obvious two-step fixes where the next action is already clear.
242
245
  - Mark completed tasks immediately and continue to the next task without yielding.
243
246
  - Delegate rather than silently shrinking scope. Prefer `executor` for bounded implementation slices, `planner` for sequencing, `architect` for architecture/code-review lanes, and `critic` for plan critique.
247
+ - Do not delegate for single-line typos, obvious syntax errors, single-file known-location fixes, or direct answers.
244
248
  </decomposition>
245
249
 
246
250
  <verification>
package/src/sdk.ts CHANGED
@@ -127,10 +127,10 @@ import {
127
127
  EditTool,
128
128
  EvalTool,
129
129
  FindTool,
130
+ getConfiguredSearchProviderPreference,
130
131
  getSearchTools,
131
132
  HIDDEN_TOOLS,
132
133
  isConfigurableSearchProviderId,
133
- isSearchProviderPreference,
134
134
  type LspStartupServerInfo,
135
135
  loadSshTool,
136
136
  ReadTool,
@@ -932,10 +932,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
932
932
  slashCommandsPromise.catch(() => {});
933
933
 
934
934
  // Initialize provider preferences from settings
935
- const webSearchProvider = settings.get("providers.webSearch");
936
- if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
937
- setPreferredSearchProvider(webSearchProvider);
938
- }
935
+ const webSearchProvider = getConfiguredSearchProviderPreference(settings);
936
+ setPreferredSearchProvider(webSearchProvider);
939
937
  const webSearchFallback = settings.get("web_search.fallback");
940
938
  if (Array.isArray(webSearchFallback)) {
941
939
  setSearchFallbackProviders(