@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 +6 -0
- package/dist/types/config/settings-schema.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +53 -0
- package/src/config/settings.ts +1 -1
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/session/agent-session.ts +70 -10
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +46 -6
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;
|
|
@@ -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.
|
|
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.
|
|
60
|
-
"@gajae-code/agent-core": "0.15.
|
|
61
|
-
"@gajae-code/ai": "0.15.
|
|
62
|
-
"@gajae-code/natives": "0.15.
|
|
63
|
-
"@gajae-code/tui": "0.15.
|
|
64
|
-
"@gajae-code/utils": "0.15.
|
|
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;
|
package/src/config/settings.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|