@gajae-code/coding-agent 0.15.3 → 0.15.4

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.
package/CHANGELOG.md CHANGED
@@ -2,10 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.4] - 2026-08-29
6
+
7
+ - The `ask` tool now rejects empty or whitespace-only custom answers on every local and remote input path. Remote invalid answers receive a visible explanation and are retried at most three times with an event-loop yield between attempts before the ask is cancelled; local empty custom input is never returned as a valid answer. (#5001)
8
+
5
9
  ## [0.15.3] - 2026-08-27
6
10
 
7
11
  ### Added
8
12
 
13
+ - Added opt-in `compaction.adaptive.*` settings and wired them into post-turn and pre-prompt auto-compaction. Adaptive mode remains disabled by default, preserves fixed-token precedence, and exposes bounded base, aggression, call-window, and minimum-threshold controls.
14
+
9
15
  - The `ask` tool declares its display-only argument fields (`questions.question`, `questions.options.label`) via `displaySafeEscapedArgFields`, so an `ask` call whose question text or option labels arrive as `\uXXXX` escapes (common from Anthropic models writing Korean or emoji) completes with a single warning instead of discarding the turn, charging the managed-fallback retry budget, and ultimately failing the run. Ids, workflow-gate metadata, and deep-interview records remain load-bearing and keep the fail-closed rejection. (#4983)
10
16
 
11
17
  - Added an explicit `Copy OAuth URL` command-palette action for interactive `/login` and runtime MCP OAuth flows. It copies the exact pending authorization URL through the configured clipboard transport only after the user selects the action; it never overwrites the clipboard automatically and clears the pending URL when the flow ends. This preserves OSC 8 policy, tmux/SSH/no-hyperlink behavior, and avoids relying on padded soft-wrapped terminal rows for copying.
@@ -2193,6 +2193,51 @@ export declare const SETTINGS_SCHEMA: {
2193
2193
  }];
2194
2194
  };
2195
2195
  };
2196
+ readonly "compaction.adaptive.enabled": {
2197
+ readonly type: "boolean";
2198
+ readonly default: false;
2199
+ readonly ui: {
2200
+ readonly tab: "context";
2201
+ readonly label: "Adaptive Compaction";
2202
+ readonly description: "Adjust compaction frequency by context size and call rate";
2203
+ };
2204
+ };
2205
+ readonly "compaction.adaptive.baseThresholdPercent": {
2206
+ readonly type: "number";
2207
+ readonly default: 85;
2208
+ readonly ui: {
2209
+ readonly tab: "context";
2210
+ readonly label: "Adaptive Base Threshold %";
2211
+ readonly description: "Base context percentage used when adaptive compaction is enabled";
2212
+ };
2213
+ };
2214
+ readonly "compaction.adaptive.aggression": {
2215
+ readonly type: "number";
2216
+ readonly default: 0.15;
2217
+ readonly ui: {
2218
+ readonly tab: "context";
2219
+ readonly label: "Adaptive Aggression";
2220
+ readonly description: "How strongly call rate lowers the compaction threshold; use 0 to 1";
2221
+ };
2222
+ };
2223
+ readonly "compaction.adaptive.turnWindow": {
2224
+ readonly type: "number";
2225
+ readonly default: 15;
2226
+ readonly ui: {
2227
+ readonly tab: "context";
2228
+ readonly label: "Call-Rate Window";
2229
+ readonly description: "Minutes of recent calls considered by adaptive compaction";
2230
+ };
2231
+ };
2232
+ readonly "compaction.adaptive.minThresholdPercent": {
2233
+ readonly type: "number";
2234
+ readonly default: 50;
2235
+ readonly ui: {
2236
+ readonly tab: "context";
2237
+ readonly label: "Adaptive Floor %";
2238
+ readonly description: "Lowest context percentage adaptive compaction may lower the threshold to";
2239
+ };
2240
+ };
2196
2241
  readonly "compaction.handoffSaveToDisk": {
2197
2242
  readonly type: "boolean";
2198
2243
  readonly default: false;
@@ -4521,6 +4566,13 @@ export interface CompactionSettings {
4521
4566
  strategy: "context-full" | "handoff" | "off";
4522
4567
  thresholdPercent: number;
4523
4568
  thresholdTokens: number;
4569
+ adaptive: {
4570
+ enabled: boolean;
4571
+ baseThresholdPercent: number;
4572
+ aggression: number;
4573
+ turnWindow: number;
4574
+ minThresholdPercent: number;
4575
+ };
4524
4576
  reserveTokens: number;
4525
4577
  keepRecentTokens: number;
4526
4578
  handoffSaveToDisk: boolean;
@@ -80,6 +80,8 @@ export interface ExtensionUIDialogOptions {
80
80
  customInput?: {
81
81
  optionLabel: string;
82
82
  onSubmit: (text: string) => void;
83
+ /** Empty/whitespace-only submissions may be rejected by the selector. */
84
+ allowEmpty?: boolean;
83
85
  };
84
86
  /**
85
87
  * Inline free-text input for a non-answer clarification action. It is
@@ -0,0 +1,6 @@
1
+ /** Shared placeholder semantics for model-authored workflow content. */
2
+ export declare const WORKFLOW_PLACEHOLDER_CORRECTION = "provide a specific, non-placeholder question or objective instead of empty, whitespace, unused, TODO, TBD, N/A, N-A, NA, none, placeholder, or stub";
3
+ /** Exact, case-insensitive placeholder detection; meaningful sentences remain valid. */
4
+ export declare function isWorkflowPlaceholderText(value: unknown): boolean;
5
+ /** Detect a legacy formatted deep-interview ask whose visible body is a placeholder. */
6
+ export declare function isLegacyDeepInterviewPlaceholder(value: unknown): boolean;
@@ -47,6 +47,7 @@ export interface HookSelectorOptions {
47
47
  customInput?: {
48
48
  optionLabel: string;
49
49
  onSubmit: (text: string) => void;
50
+ allowEmpty?: boolean;
50
51
  };
51
52
  clarificationInput?: {
52
53
  optionLabel: string;
@@ -45,6 +45,7 @@ export interface AskToolDetails {
45
45
  export declare const OTHER_OPTION = "Other (type your own)";
46
46
  export declare const ASK_CLARIFICATION_OPTION = "Ask about these choices";
47
47
  export declare const RECOMMENDED_SUFFIX = " (Recommended)";
48
+ export declare const MAX_EMPTY_CUSTOM_ATTEMPTS = 3;
48
49
  /** The only remote navigation control; labels are presentation, never protocol. */
49
50
  export declare function askRemoteControls(input: {
50
51
  multi: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.15.3",
4
+ "version": "0.15.4",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -56,12 +56,12 @@
56
56
  "@agentclientprotocol/sdk": "1.3.0",
57
57
  "@babel/parser": "^7.29.3",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@gajae-code/stats": "0.15.3",
60
- "@gajae-code/agent-core": "0.15.3",
61
- "@gajae-code/ai": "0.15.3",
62
- "@gajae-code/natives": "0.15.3",
63
- "@gajae-code/tui": "0.15.3",
64
- "@gajae-code/utils": "0.15.3",
59
+ "@gajae-code/stats": "0.15.4",
60
+ "@gajae-code/agent-core": "0.15.4",
61
+ "@gajae-code/ai": "0.15.4",
62
+ "@gajae-code/natives": "0.15.4",
63
+ "@gajae-code/tui": "0.15.4",
64
+ "@gajae-code/utils": "0.15.4",
65
65
  "@puppeteer/browsers": "^2.13.0",
66
66
  "@types/turndown": "5.0.6",
67
67
  "@xterm/headless": "^6.0.0",
@@ -1940,6 +1940,52 @@ export const SETTINGS_SCHEMA = {
1940
1940
  },
1941
1941
  },
1942
1942
 
1943
+ "compaction.adaptive.enabled": {
1944
+ type: "boolean",
1945
+ default: false,
1946
+ ui: {
1947
+ tab: "context",
1948
+ label: "Adaptive Compaction",
1949
+ description: "Adjust compaction frequency by context size and call rate",
1950
+ },
1951
+ },
1952
+ "compaction.adaptive.baseThresholdPercent": {
1953
+ type: "number",
1954
+ default: 85,
1955
+ ui: {
1956
+ tab: "context",
1957
+ label: "Adaptive Base Threshold %",
1958
+ description: "Base context percentage used when adaptive compaction is enabled",
1959
+ },
1960
+ },
1961
+ "compaction.adaptive.aggression": {
1962
+ type: "number",
1963
+ default: 0.15,
1964
+ ui: {
1965
+ tab: "context",
1966
+ label: "Adaptive Aggression",
1967
+ description: "How strongly call rate lowers the compaction threshold; use 0 to 1",
1968
+ },
1969
+ },
1970
+ "compaction.adaptive.turnWindow": {
1971
+ type: "number",
1972
+ default: 15,
1973
+ ui: {
1974
+ tab: "context",
1975
+ label: "Call-Rate Window",
1976
+ description: "Minutes of recent calls considered by adaptive compaction",
1977
+ },
1978
+ },
1979
+ "compaction.adaptive.minThresholdPercent": {
1980
+ type: "number",
1981
+ default: 50,
1982
+ ui: {
1983
+ tab: "context",
1984
+ label: "Adaptive Floor %",
1985
+ description: "Lowest context percentage adaptive compaction may lower the threshold to",
1986
+ },
1987
+ },
1988
+
1943
1989
  "compaction.handoffSaveToDisk": {
1944
1990
  type: "boolean",
1945
1991
  default: false,
@@ -4332,6 +4378,13 @@ export interface CompactionSettings {
4332
4378
  strategy: "context-full" | "handoff" | "off";
4333
4379
  thresholdPercent: number;
4334
4380
  thresholdTokens: number;
4381
+ adaptive: {
4382
+ enabled: boolean;
4383
+ baseThresholdPercent: number;
4384
+ aggression: number;
4385
+ turnWindow: number;
4386
+ minThresholdPercent: number;
4387
+ };
4335
4388
  reserveTokens: number;
4336
4389
  keepRecentTokens: number;
4337
4390
  handoffSaveToDisk: boolean;
@@ -1174,7 +1174,7 @@ export class Settings implements NotificationSettingsReader {
1174
1174
  for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
1175
1175
  if (key.startsWith(`${prefix}.`)) {
1176
1176
  const suffix = key.slice(prefix.length + 1);
1177
- result[suffix] = this.get(key);
1177
+ setByPath(result, suffix.split("."), this.get(key));
1178
1178
  }
1179
1179
  }
1180
1180
  return result as unknown as GroupTypeMap[G];
@@ -10,6 +10,7 @@ import {
10
10
  type ViewportAnchorSource,
11
11
  type ViewportAnchorSourceRenderer,
12
12
  } from "@gajae-code/tui";
13
+ import { isLegacyDeepInterviewPlaceholder, isWorkflowPlaceholderText } from "../gjc-runtime/workflow-placeholder";
13
14
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
14
15
 
15
16
  interface RoundQuestionModel {
@@ -132,7 +133,7 @@ function parseRoundQuestion(text: string): RoundQuestionModel | null {
132
133
  .slice(headerIndex + 1)
133
134
  .join("\n")
134
135
  .trim();
135
- if (!body) return null;
136
+ if (isWorkflowPlaceholderText(body)) return null;
136
137
 
137
138
  const componentMatch =
138
139
  /^Round\s+(\d+)\s+\|\s+Component:\s*(.*?)\s+\|\s+Targeting:\s*(.*?)\s+\|\s+Why now:\s*(.*?)\s+\|\s+Ambiguity:\s*(.+?)%?\s*$/i.exec(
@@ -204,11 +205,13 @@ function parseTopologyQuestion(text: string): TopologyQuestionModel | null {
204
205
  if (inQuestion) questionLines.push(trimmed);
205
206
  else contextLines.push(trimmed);
206
207
  }
208
+ const question = questionLines.join("\n");
209
+ if (isWorkflowPlaceholderText(question)) return null;
207
210
  return {
208
211
  kind: "topology-question",
209
212
  context: contextLines.join("\n") || undefined,
210
213
  components,
211
- question: questionLines.join("\n"),
214
+ question,
212
215
  };
213
216
  }
214
217
 
@@ -399,6 +402,7 @@ export function renderDeepInterviewAskQuestion(question: string, uiTheme: Theme)
399
402
  export function isDeepInterviewAskQuestion(question: string): boolean {
400
403
  if (parseTopologyQuestion(question) ?? parseRoundQuestion(question)) return true;
401
404
  const normalized = normalizeText(question);
405
+ if (isLegacyDeepInterviewPlaceholder(normalized)) return false;
402
406
  return /(?:^|\n)\s*Round\s+\d+\s*\|.*?\bAmbiguity\b/i.test(normalized);
403
407
  }
404
408
 
@@ -155,6 +155,8 @@ export interface ExtensionUIDialogOptions {
155
155
  customInput?: {
156
156
  optionLabel: string;
157
157
  onSubmit: (text: string) => void;
158
+ /** Empty/whitespace-only submissions may be rejected by the selector. */
159
+ allowEmpty?: boolean;
158
160
  };
159
161
  /**
160
162
  * Inline free-text input for a non-answer clarification action. It is
@@ -747,7 +747,12 @@ export async function resolveRalplanTargetRoot(
747
747
  if (!repository) {
748
748
  throw new RalplanCommandError(2, `ralplan --worktree-root is not inside a git repository: ${canonical}`);
749
749
  }
750
- const verified = Bun.spawn(["git", "-C", canonical, "rev-parse", "--verify", "HEAD^{commit}"], {
750
+ // Do not pass the caret-containing `HEAD^{commit}` revision here. On Windows,
751
+ // Bun may dispatch git through a `.cmd` shim, where `^` is interpreted by the
752
+ // command shell before git receives the argument. HEAD is sufficient after
753
+ // resolving a repository root: an unborn or non-commit HEAD still fails the
754
+ // same validation, while the argv remains shell-safe on every platform.
755
+ const verified = Bun.spawn(["git", "-C", canonical, "rev-parse", "--verify", "HEAD"], {
751
756
  stdout: "pipe",
752
757
  stderr: "pipe",
753
758
  });
@@ -86,6 +86,7 @@ import {
86
86
  type UltragoalValidationLaneSelection,
87
87
  validationLaneSelectionFor,
88
88
  } from "./ultragoal-validation-policy";
89
+ import { isWorkflowPlaceholderText, WORKFLOW_PLACEHOLDER_CORRECTION } from "./workflow-placeholder";
89
90
  import { resolveWorkflowSetting } from "./workflow-settings";
90
91
 
91
92
  export {
@@ -1166,6 +1167,9 @@ function parseGoalsFromBrief(brief: string): ParsedGoal[] {
1166
1167
  if (!title && !body) {
1167
1168
  throw new Error(`ultragoal @goal block ${index + 1} has no title or objective`);
1168
1169
  }
1170
+ if (isWorkflowPlaceholderText(body || title)) {
1171
+ throw new Error(`ultragoal @goal block ${index + 1} requires ${WORKFLOW_PLACEHOLDER_CORRECTION}`);
1172
+ }
1169
1173
  return { title: clampTitle(title), objective: body || title };
1170
1174
  });
1171
1175
  }
@@ -1180,6 +1184,7 @@ export async function createUltragoalPlan(input: {
1180
1184
  }): Promise<UltragoalPlan> {
1181
1185
  const brief = input.brief.trim();
1182
1186
  if (!brief) throw new Error("ultragoal brief is required");
1187
+ if (isWorkflowPlaceholderText(brief)) throw new Error(`ultragoal brief requires ${WORKFLOW_PLACEHOLDER_CORRECTION}`);
1183
1188
  const now = new Date().toISOString();
1184
1189
  // Parse the untrimmed brief so the raw-line delimiter contract holds: a
1185
1190
  // leading-indented `@goal` on the first line must stay objective text rather
@@ -0,0 +1,33 @@
1
+ /** Shared placeholder semantics for model-authored workflow content. */
2
+
3
+ const WORKFLOW_PLACEHOLDER_TOKENS = new Set([
4
+ "empty",
5
+ "n/a",
6
+ "n-a",
7
+ "na",
8
+ "none",
9
+ "placeholder",
10
+ "stub",
11
+ "tbd",
12
+ "todo",
13
+ "unused",
14
+ ]);
15
+
16
+ export const WORKFLOW_PLACEHOLDER_CORRECTION =
17
+ "provide a specific, non-placeholder question or objective instead of empty, whitespace, unused, TODO, TBD, N/A, N-A, NA, none, placeholder, or stub";
18
+
19
+ /** Exact, case-insensitive placeholder detection; meaningful sentences remain valid. */
20
+ export function isWorkflowPlaceholderText(value: unknown): boolean {
21
+ if (typeof value !== "string") return false;
22
+ const normalized = value.trim().toLowerCase();
23
+ return normalized.length === 0 || WORKFLOW_PLACEHOLDER_TOKENS.has(normalized);
24
+ }
25
+
26
+ /** Detect a legacy formatted deep-interview ask whose visible body is a placeholder. */
27
+ export function isLegacyDeepInterviewPlaceholder(value: unknown): boolean {
28
+ if (typeof value !== "string") return false;
29
+ const lines = value.trim().split(/\r?\n/);
30
+ const headerIndex = lines.findIndex(line => /^Round\s+\d+\s+\|.*\bAmbiguity\b/i.test(line.trim()));
31
+ if (headerIndex < 0) return false;
32
+ return isWorkflowPlaceholderText(lines.slice(headerIndex + 1).join("\n"));
33
+ }