@sayknow-cli/coding-agent 0.3.8 → 0.3.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.
- package/CHANGELOG.md +13 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +11 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +15 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +7 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/notifications/config-commands.d.ts +38 -0
- package/dist/types/notifications/config.d.ts +16 -0
- package/dist/types/notifications/index.d.ts +14 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +22 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +10 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/package.json +7 -7
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +38 -10
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +107 -11
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +11 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +26 -3
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +83 -20
- package/src/modes/controllers/event-controller.ts +11 -2
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +62 -26
- package/src/modes/controllers/selector-controller.ts +43 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +3 -9
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +90 -0
- package/src/notifications/config.ts +27 -0
- package/src/notifications/index.ts +184 -14
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +143 -0
- package/src/notifications/telegram-daemon.ts +431 -91
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +42 -1
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +104 -13
- package/src/session/agent-session.ts +48 -22
- package/src/skc-runtime/psmux-detect.ts +17 -2
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-runtime.ts +10 -1
- package/src/slash-commands/builtin-registry.ts +78 -1
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
- package/src/utils/pasted-image-path.ts +170 -0
|
@@ -143,6 +143,23 @@ export function buildActionMessage(action: {
|
|
|
143
143
|
return { text: body, inline_keyboard };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** Render an `action_needed` body as raw markdown (rich-message source; the HTML fallback stays on buildActionMessage). */
|
|
147
|
+
export function buildActionMarkdown(action: {
|
|
148
|
+
kind: "ask" | "idle";
|
|
149
|
+
question?: string;
|
|
150
|
+
options?: string[];
|
|
151
|
+
summary?: string;
|
|
152
|
+
}): string {
|
|
153
|
+
if (action.kind === "idle") {
|
|
154
|
+
return action.summary ? `🟢 Agent idle\n${action.summary}` : "🟢 Agent idle";
|
|
155
|
+
}
|
|
156
|
+
const heading = `❓ **${action.question ?? "Question"}**`;
|
|
157
|
+
const options = action.options ?? [];
|
|
158
|
+
if (options.length === 0) return `${heading}\n\n(reply with text)`;
|
|
159
|
+
const list = options.map((label, i) => `${i + 1}. ${label.replace(/^\s*\d+[.)]\s+/, "")}`).join("\n");
|
|
160
|
+
return `${heading}\n\n${list}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
/** Send Telegram HTML text chunks sequentially so long messages preserve order. */
|
|
147
164
|
export async function sendTelegramHtmlChunks(
|
|
148
165
|
send: TelegramSend,
|
|
@@ -38,6 +38,13 @@ export interface ThreadedSend {
|
|
|
38
38
|
* message. Set for streamed turn frames so live + finalized share one message.
|
|
39
39
|
*/
|
|
40
40
|
editable?: boolean;
|
|
41
|
+
/** Rich message class metadata. Only finalized final-answer sends are ever
|
|
42
|
+
* rich-promoted; the daemon gate (`shouldPromoteRich`) requires exactly this. */
|
|
43
|
+
richClass?: "final";
|
|
44
|
+
/** Rich final-answer markdown (raw). Delivery marker derived ONLY from a frame's `finalAnswer` bit; never inferred from `phase`. */
|
|
45
|
+
richMarkdown?: string;
|
|
46
|
+
/** Live-turn raw markdown for opt-in draft streaming (set ONLY on non-finalized turn frames; never triggers rich-final promotion, which requires `lane === "finalized"`). */
|
|
47
|
+
richDraftMarkdown?: string;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
interface ThreadedFrame {
|
|
@@ -58,6 +65,7 @@ interface ThreadedFrame {
|
|
|
58
65
|
cwd?: unknown;
|
|
59
66
|
// turn_stream
|
|
60
67
|
phase?: unknown;
|
|
68
|
+
finalAnswer?: boolean;
|
|
61
69
|
text?: unknown;
|
|
62
70
|
messageRef?: unknown;
|
|
63
71
|
// image_attachment / file_attachment
|
|
@@ -69,12 +77,19 @@ interface ThreadedFrame {
|
|
|
69
77
|
// config_update
|
|
70
78
|
verbosity?: unknown;
|
|
71
79
|
redact?: unknown;
|
|
80
|
+
// control_command_result
|
|
81
|
+
status?: unknown;
|
|
82
|
+
message?: unknown;
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
function str(v: unknown): string | undefined {
|
|
75
86
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function isDotOnlyText(value: string): boolean {
|
|
90
|
+
return /^[.\s]+$/.test(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
/** Format the one-time identity header as pinned bullets. */
|
|
79
94
|
export function formatIdentityHeader(frame: {
|
|
80
95
|
repo?: unknown;
|
|
@@ -139,6 +154,7 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
|
|
|
139
154
|
case "turn_stream": {
|
|
140
155
|
const raw = str(frame.text);
|
|
141
156
|
if (!raw) return undefined;
|
|
157
|
+
if (frame.phase === "finalized" && isDotOnlyText(raw)) return undefined;
|
|
142
158
|
const text = markdownToTelegramHtml(raw);
|
|
143
159
|
const finalized = frame.phase === "finalized";
|
|
144
160
|
// A per-turn ref ties the streamed live edits and the finalized text to
|
|
@@ -156,6 +172,16 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
|
|
|
156
172
|
text,
|
|
157
173
|
coalesceKey,
|
|
158
174
|
editable: coalesceKey !== undefined,
|
|
175
|
+
// Rich-final markers are set ONLY on a non-editable finalized final
|
|
176
|
+
// (no messageRef / coalesceKey). A streamed (editable) final owns a live
|
|
177
|
+
// message edited in place, so it is never rich-promoted and carries no
|
|
178
|
+
// rich marker to leak into split continuations.
|
|
179
|
+
richClass: frame.finalAnswer === true && coalesceKey === undefined ? "final" : undefined,
|
|
180
|
+
richMarkdown: frame.finalAnswer === true && coalesceKey === undefined ? raw : undefined,
|
|
181
|
+
// Live-only draft marker: carries the RAW markdown on non-finalized turn
|
|
182
|
+
// frames so the opt-in draft gate has the source. It never arms rich-final
|
|
183
|
+
// promotion (shouldPromoteRich requires lane === "finalized").
|
|
184
|
+
richDraftMarkdown: finalized ? undefined : raw,
|
|
159
185
|
};
|
|
160
186
|
}
|
|
161
187
|
case "image_attachment": {
|
|
@@ -188,9 +214,24 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
|
|
|
188
214
|
const redact = typeof frame.redact === "boolean" ? `redact ${frame.redact ? "on" : "off"}` : undefined;
|
|
189
215
|
const parts = [verbosity ? `verbosity ${verbosity}` : undefined, redact].filter(Boolean);
|
|
190
216
|
return parts.length
|
|
191
|
-
? {
|
|
217
|
+
? {
|
|
218
|
+
method: "sendMessage",
|
|
219
|
+
lane: "idle",
|
|
220
|
+
text: finalizeTelegramHtml(`⚙ ${escapeHtml(parts.join(", "))}`),
|
|
221
|
+
}
|
|
192
222
|
: undefined;
|
|
193
223
|
}
|
|
224
|
+
case "control_command_result": {
|
|
225
|
+
const message = str(frame.message);
|
|
226
|
+
if (!message) return undefined;
|
|
227
|
+
const status = str(frame.status);
|
|
228
|
+
const prefix = status === "ok" ? "✅" : status === "unavailable" ? "⚠️" : "❌";
|
|
229
|
+
return {
|
|
230
|
+
method: "sendMessage",
|
|
231
|
+
lane: "idle",
|
|
232
|
+
text: finalizeTelegramHtml(`${prefix} ${escapeHtml(truncate(message, 1200))}`),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
194
235
|
default:
|
|
195
236
|
return undefined;
|
|
196
237
|
}
|
|
@@ -120,15 +120,16 @@ export class TopicRegistry {
|
|
|
120
120
|
return record ? !record.identitySent : true;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
* caller should `editForumTopic`), `false` when already current or unknown.
|
|
126
|
-
*/
|
|
127
|
-
applyName(sessionId: string, name: string): boolean {
|
|
123
|
+
/** Whether a known session topic's applied title differs from `name`. */
|
|
124
|
+
needsRename(sessionId: string, name: string): boolean {
|
|
128
125
|
const record = this.topics.get(sessionId);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
return record !== undefined && record.name !== name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Commit a successfully-applied Telegram topic title. */
|
|
130
|
+
markNameApplied(sessionId: string, name: string): void {
|
|
131
|
+
const record = this.topics.get(sessionId);
|
|
132
|
+
if (record) record.name = name;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
/** Remove a session topic record after Telegram deletes the topic. */
|
|
@@ -21,3 +21,8 @@ Returns JSON with:
|
|
|
21
21
|
- `activated_tools` — tools activated by this search call
|
|
22
22
|
- `match_count` — number of ranked matches returned by the search
|
|
23
23
|
- `total_tools`
|
|
24
|
+
|
|
25
|
+
Match details include:
|
|
26
|
+
- `server_name` — MCP server name when the activated result is an MCP tool
|
|
27
|
+
- `mcp_tool_name` — original MCP tool name when applicable
|
|
28
|
+
- `schema_keys` — searchable input property names
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Discover project and user runtime skills without loading full skill content.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- Searches only custom runtime skill locations: project `.skc/skills` and user `~/.skc/skills`.
|
|
5
|
+
- Built-in, bundled, and internal workflow skills are intentionally excluded.
|
|
6
|
+
- Returns thin metadata only: name, description, source scope, path, and use conditions when present.
|
|
7
|
+
- To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.
|
|
8
|
+
</instruction>
|
|
9
|
+
|
|
10
|
+
Input:
|
|
11
|
+
- `query` (optional): words to match against skill name, description, source, or use conditions.
|
|
12
|
+
- `source` (optional): `all`, `project`, or `user`.
|
|
13
|
+
- `limit` (optional): maximum results, 1-50.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AuthCredentialSelector, AuthCredentialSelectorKind } from "@sayknow-cli/ai";
|
|
2
|
+
|
|
3
|
+
export interface CliCredentialSelector {
|
|
4
|
+
provider?: string;
|
|
5
|
+
selector: AuthCredentialSelector;
|
|
6
|
+
raw: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SELECTOR_KINDS = new Set<AuthCredentialSelectorKind>(["id", "email", "account", "project"]);
|
|
10
|
+
|
|
11
|
+
function parseSelectorBody(body: string): AuthCredentialSelector | undefined {
|
|
12
|
+
const separator = body.indexOf(":");
|
|
13
|
+
if (separator === -1) {
|
|
14
|
+
if (body.includes("@")) return { kind: "email", value: body };
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const rawKind = body.slice(0, separator);
|
|
18
|
+
const value = body.slice(separator + 1).trim();
|
|
19
|
+
if (!SELECTOR_KINDS.has(rawKind as AuthCredentialSelectorKind) || value.length === 0) return undefined;
|
|
20
|
+
return { kind: rawKind as AuthCredentialSelectorKind, value };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseCliCredentialSelector(raw: string): CliCredentialSelector {
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
const slash = trimmed.indexOf("/");
|
|
26
|
+
const provider = slash === -1 ? undefined : trimmed.slice(0, slash).trim();
|
|
27
|
+
const body = slash === -1 ? trimmed : trimmed.slice(slash + 1).trim();
|
|
28
|
+
const selector = parseSelectorBody(body);
|
|
29
|
+
if (!selector || provider === "") {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid --credential selector "${raw}". Use email:name@example.com, id:123, account:<id>, project:<id>, or provider/email:name@example.com.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return { ...(provider ? { provider } : {}), selector, raw };
|
|
35
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@sayknow-cli/agent-core";
|
|
11
11
|
import {
|
|
12
12
|
type Api,
|
|
13
|
+
type AuthCredentialSelector,
|
|
13
14
|
type CredentialDisabledEvent,
|
|
14
15
|
type Message,
|
|
15
16
|
type Model,
|
|
@@ -40,7 +41,7 @@ import {
|
|
|
40
41
|
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
|
|
41
42
|
import { loadCapability } from "./capability";
|
|
42
43
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
43
|
-
import { ModelRegistry } from "./config/model-registry";
|
|
44
|
+
import { kNoAuth, ModelRegistry } from "./config/model-registry";
|
|
44
45
|
import {
|
|
45
46
|
defaultModelPerProvider,
|
|
46
47
|
formatModelString,
|
|
@@ -123,7 +124,12 @@ import {
|
|
|
123
124
|
} from "./system-prompt";
|
|
124
125
|
import { AgentOutputManager } from "./task/output-manager";
|
|
125
126
|
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
|
|
126
|
-
import {
|
|
127
|
+
import {
|
|
128
|
+
collectDiscoverableTools,
|
|
129
|
+
type DiscoverableTool,
|
|
130
|
+
isMCPBridgeTool,
|
|
131
|
+
isMCPToolName,
|
|
132
|
+
} from "./tool-discovery/tool-index";
|
|
127
133
|
import {
|
|
128
134
|
applyConfiguredSearchTimeout,
|
|
129
135
|
BashTool,
|
|
@@ -269,6 +275,8 @@ export interface CreateAgentSessionOptions {
|
|
|
269
275
|
/** Optional provider-facing session identifier for prompt caches and sticky auth selection.
|
|
270
276
|
* Keeps persisted session files isolated while reusing provider-side caches. */
|
|
271
277
|
providerSessionId?: string;
|
|
278
|
+
/** Runtime credential selector for multi-account auth pools. */
|
|
279
|
+
credentialSelector?: { provider?: string; selector: AuthCredentialSelector; raw: string };
|
|
272
280
|
|
|
273
281
|
/** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
|
|
274
282
|
customTools?: (CustomTool | ToolDefinition)[];
|
|
@@ -900,10 +908,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
900
908
|
startupCredentialDisabledEvents.push(event);
|
|
901
909
|
}
|
|
902
910
|
});
|
|
911
|
+
let runtimeCredentialSelectorInstalled = false;
|
|
912
|
+
const installRuntimeCredentialSelector = (provider: string): void => {
|
|
913
|
+
if (!options.credentialSelector || runtimeCredentialSelectorInstalled) return;
|
|
914
|
+
authStorage.setRuntimeCredentialSelector(provider, options.credentialSelector.selector);
|
|
915
|
+
runtimeCredentialSelectorInstalled = true;
|
|
916
|
+
};
|
|
917
|
+
const earlyCredentialSelectorProvider = options.credentialSelector?.provider ?? options.model?.provider;
|
|
918
|
+
if (earlyCredentialSelectorProvider) {
|
|
919
|
+
installRuntimeCredentialSelector(earlyCredentialSelectorProvider);
|
|
920
|
+
}
|
|
903
921
|
const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
|
|
904
922
|
modelRegistry.applyConfiguredModelBindings(settings);
|
|
905
923
|
logger.time("initializeWithSettings", initializeWithSettings, settings);
|
|
906
|
-
|
|
924
|
+
const canRefreshModelsBeforeCredentialSelector =
|
|
925
|
+
!options.credentialSelector || runtimeCredentialSelectorInstalled || options.modelRegistry !== undefined;
|
|
926
|
+
if (!options.modelRegistry && canRefreshModelsBeforeCredentialSelector) {
|
|
907
927
|
modelRegistry.refreshInBackground();
|
|
908
928
|
}
|
|
909
929
|
// Kick off workspace tree discovery early. The native workspace scan returns
|
|
@@ -969,7 +989,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
969
989
|
return cached;
|
|
970
990
|
}
|
|
971
991
|
|
|
972
|
-
const
|
|
992
|
+
const credentialSelector =
|
|
993
|
+
options.credentialSelector && !runtimeCredentialSelectorInstalled
|
|
994
|
+
? options.credentialSelector.provider === undefined ||
|
|
995
|
+
options.credentialSelector.provider === candidate.provider
|
|
996
|
+
? options.credentialSelector.selector
|
|
997
|
+
: undefined
|
|
998
|
+
: undefined;
|
|
999
|
+
if (options.credentialSelector?.provider && options.credentialSelector.provider !== candidate.provider) {
|
|
1000
|
+
modelApiKeyAvailability.set(availabilityKey, false);
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
const key = await modelRegistry.getApiKey(candidate, providerSessionId, { credentialSelector }).catch(error => {
|
|
1004
|
+
if (credentialSelector) {
|
|
1005
|
+
logger.debug("Credential selector did not match model availability candidate", {
|
|
1006
|
+
provider: candidate.provider,
|
|
1007
|
+
model: candidate.id,
|
|
1008
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1009
|
+
});
|
|
1010
|
+
return undefined;
|
|
1011
|
+
}
|
|
1012
|
+
throw error;
|
|
1013
|
+
});
|
|
1014
|
+
const hasKey = Boolean(key) && (!credentialSelector || key !== kNoAuth);
|
|
973
1015
|
modelApiKeyAvailability.set(availabilityKey, hasKey);
|
|
974
1016
|
return hasKey;
|
|
975
1017
|
};
|
|
@@ -1044,6 +1086,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1044
1086
|
const taskDepth = options.taskDepth ?? 0;
|
|
1045
1087
|
|
|
1046
1088
|
let thinkingLevel = options.thinkingLevel;
|
|
1089
|
+
const hasExplicitDefaultThinkingLevel = settings.has("defaultThinkingLevel");
|
|
1090
|
+
let thinkingLevelFromSchemaDefault = false;
|
|
1047
1091
|
|
|
1048
1092
|
// If session has data and includes a thinking entry, restore it
|
|
1049
1093
|
if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
|
|
@@ -1054,13 +1098,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1054
1098
|
thinkingLevel = defaultRoleSpec.thinkingLevel;
|
|
1055
1099
|
}
|
|
1056
1100
|
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1101
|
+
// An explicit user/project default should win over the model's bundled
|
|
1102
|
+
// defaultLevel. The schema default is only a final fallback so model metadata
|
|
1103
|
+
// can keep driving first-run behavior until the user chooses "Set as default".
|
|
1104
|
+
if (thinkingLevel === undefined && hasExplicitDefaultThinkingLevel) {
|
|
1105
|
+
thinkingLevel = settings.get("defaultThinkingLevel");
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1059
1108
|
if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) {
|
|
1060
1109
|
thinkingLevel = model.thinking.defaultLevel;
|
|
1061
1110
|
}
|
|
1111
|
+
|
|
1062
1112
|
if (thinkingLevel === undefined) {
|
|
1063
1113
|
thinkingLevel = settings.get("defaultThinkingLevel");
|
|
1114
|
+
thinkingLevelFromSchemaDefault = true;
|
|
1064
1115
|
}
|
|
1065
1116
|
if (model) {
|
|
1066
1117
|
const resolvedModel = model;
|
|
@@ -1605,6 +1656,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1605
1656
|
if (resolved) {
|
|
1606
1657
|
model = resolved;
|
|
1607
1658
|
modelFallbackMessage = undefined;
|
|
1659
|
+
if (thinkingLevelFromSchemaDefault && resolved.thinking?.defaultLevel !== undefined) {
|
|
1660
|
+
thinkingLevel = resolved.thinking.defaultLevel;
|
|
1661
|
+
thinkingLevelFromSchemaDefault = false;
|
|
1662
|
+
}
|
|
1663
|
+
thinkingLevel = resolveThinkingLevelForModel(resolved, thinkingLevel);
|
|
1608
1664
|
} else {
|
|
1609
1665
|
modelFallbackMessage = `Model "${options.modelPattern}" not found`;
|
|
1610
1666
|
}
|
|
@@ -1657,6 +1713,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1657
1713
|
}
|
|
1658
1714
|
}
|
|
1659
1715
|
|
|
1716
|
+
if (options.credentialSelector && !runtimeCredentialSelectorInstalled) {
|
|
1717
|
+
const credentialProvider = options.credentialSelector.provider ?? model?.provider;
|
|
1718
|
+
if (!credentialProvider) {
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
`--credential ${options.credentialSelector.raw} requires a resolved model or an explicit provider prefix`,
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
installRuntimeCredentialSelector(credentialProvider);
|
|
1724
|
+
if (!options.modelRegistry && !canRefreshModelsBeforeCredentialSelector) {
|
|
1725
|
+
modelRegistry.refreshInBackground();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1660
1728
|
const customCommandsResult: CustomCommandsLoadResult = { commands: [], errors: [] };
|
|
1661
1729
|
|
|
1662
1730
|
let extensionRunner: ExtensionRunner | undefined;
|
|
@@ -1812,7 +1880,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1812
1880
|
{ source: "builtin" },
|
|
1813
1881
|
)
|
|
1814
1882
|
: [];
|
|
1815
|
-
const
|
|
1883
|
+
const discoverableMCPTools: DiscoverableTool[] = mcpDiscoveryEnabled
|
|
1884
|
+
? collectDiscoverableTools(
|
|
1885
|
+
Array.from(tools.values()).filter(
|
|
1886
|
+
tool => isMCPToolName(tool.name) && !activeToolNames.has(tool.name),
|
|
1887
|
+
),
|
|
1888
|
+
{ source: "mcp" },
|
|
1889
|
+
)
|
|
1890
|
+
: [];
|
|
1891
|
+
const discoverableToolsForDesc: DiscoverableTool[] = [...discoverableBuiltinTools, ...discoverableMCPTools];
|
|
1816
1892
|
const promptTools = buildSystemPromptToolMetadata(tools, {
|
|
1817
1893
|
search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
|
|
1818
1894
|
});
|
|
@@ -1861,8 +1937,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1861
1937
|
intentField,
|
|
1862
1938
|
mcpDiscoveryMode: false,
|
|
1863
1939
|
mcpDiscoveryServerSummaries: [],
|
|
1864
|
-
toolDiscoveryActive: effectiveDiscoveryMode === "all",
|
|
1865
|
-
discoverableTools: discoverableBuiltinTools
|
|
1940
|
+
toolDiscoveryActive: effectiveDiscoveryMode === "all" || mcpDiscoveryEnabled,
|
|
1941
|
+
discoverableTools: [...discoverableBuiltinTools, ...discoverableMCPTools]
|
|
1866
1942
|
.map(tool => ({ name: tool.name, summary: tool.summary }))
|
|
1867
1943
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1868
1944
|
eagerTasks,
|
|
@@ -1893,11 +1969,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1893
1969
|
: toolNamesFromRegistry;
|
|
1894
1970
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1895
1971
|
const requestedToolNameSet = new Set(normalizedRequested);
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1972
|
+
// Generic tool discovery hides non-essential built-ins behind search_tool_bm25.
|
|
1973
|
+
// Legacy MCP discovery remains separately controlled by mcp.discoveryMode so
|
|
1974
|
+
// loaded MCP catalogs can be searched without enabling every hidden built-in.
|
|
1898
1975
|
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
|
|
1899
1976
|
const effectiveDiscoveryMode: "off" | "all" = toolsDiscoveryModeSetting === "all" ? "all" : "off";
|
|
1900
|
-
const mcpDiscoveryEnabled =
|
|
1977
|
+
const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") === true;
|
|
1901
1978
|
const defaultInactiveToolNames = new Set(
|
|
1902
1979
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
1903
1980
|
);
|
|
@@ -1910,6 +1987,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1910
1987
|
const discoveryDefaultServerToolNames: string[] = [];
|
|
1911
1988
|
let initialSelectedMCPToolNames: string[] = [];
|
|
1912
1989
|
let defaultSelectedMCPToolNames: string[] = [];
|
|
1990
|
+
if (mcpDiscoveryEnabled) {
|
|
1991
|
+
const defaultServerNames = new Set(settings.get("mcp.discoveryDefaultServers") ?? []);
|
|
1992
|
+
for (const tool of toolRegistry.values()) {
|
|
1993
|
+
if (!isMCPBridgeTool(tool)) continue;
|
|
1994
|
+
discoverableMCPToolNames.add(tool.name);
|
|
1995
|
+
if (initialRequestedActiveToolNames.includes(tool.name)) {
|
|
1996
|
+
explicitlyRequestedMCPToolNames.push(tool.name);
|
|
1997
|
+
}
|
|
1998
|
+
const serverName = (tool as AgentTool & { mcpServerName?: string }).mcpServerName;
|
|
1999
|
+
if (serverName && defaultServerNames.has(serverName)) {
|
|
2000
|
+
discoveryDefaultServerToolNames.push(tool.name);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1913
2004
|
let initialToolNames = [...initialRequestedActiveToolNames];
|
|
1914
2005
|
if (mcpDiscoveryEnabled) {
|
|
1915
2006
|
const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name =>
|
|
@@ -2261,7 +2352,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2261
2352
|
initialSelectedMCPToolNames,
|
|
2262
2353
|
defaultSelectedMCPToolNames,
|
|
2263
2354
|
persistInitialMCPToolSelection: !hasExistingSession,
|
|
2264
|
-
defaultSelectedMCPServerNames: [],
|
|
2355
|
+
defaultSelectedMCPServerNames: settings.get("mcp.discoveryDefaultServers") ?? [],
|
|
2265
2356
|
ttsrManager,
|
|
2266
2357
|
obfuscator,
|
|
2267
2358
|
agentId: resolvedAgentId,
|
|
@@ -473,6 +473,8 @@ export interface PromptOptions {
|
|
|
473
473
|
images?: ImageContent[];
|
|
474
474
|
/** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */
|
|
475
475
|
streamingBehavior?: "steer" | "followUp";
|
|
476
|
+
/** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
|
|
477
|
+
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
476
478
|
/** Optional tool choice override for the next LLM call. */
|
|
477
479
|
toolChoice?: ToolChoice;
|
|
478
480
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
@@ -1786,7 +1788,7 @@ export class AgentSession {
|
|
|
1786
1788
|
high = mid - 1;
|
|
1787
1789
|
}
|
|
1788
1790
|
}
|
|
1789
|
-
return { message: bestMessage, tokens:
|
|
1791
|
+
return { message: bestMessage, tokens: bestTokens };
|
|
1790
1792
|
};
|
|
1791
1793
|
|
|
1792
1794
|
for (let i = providerMessages.length - 1; i >= 0; i--) {
|
|
@@ -1806,9 +1808,11 @@ export class AgentSession {
|
|
|
1806
1808
|
recordSkip("token-limit");
|
|
1807
1809
|
if (selected.length === 0) {
|
|
1808
1810
|
const truncated = truncateMessageToTokenBudget(sanitized);
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1811
|
+
if (truncated.tokens <= maxTokens) {
|
|
1812
|
+
selected.unshift(truncated.message);
|
|
1813
|
+
approximateTokens = truncated.tokens;
|
|
1814
|
+
skippedReasons["newest-message-truncated"] = (skippedReasons["newest-message-truncated"] ?? 0) + 1;
|
|
1815
|
+
}
|
|
1812
1816
|
}
|
|
1813
1817
|
break;
|
|
1814
1818
|
}
|
|
@@ -5302,7 +5306,9 @@ export class AgentSession {
|
|
|
5302
5306
|
throw new AgentBusyError();
|
|
5303
5307
|
}
|
|
5304
5308
|
if (options.streamingBehavior === "followUp") {
|
|
5305
|
-
await this.#queueFollowUp(expandedText, options?.images
|
|
5309
|
+
await this.#queueFollowUp(expandedText, options?.images, {
|
|
5310
|
+
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
5311
|
+
});
|
|
5306
5312
|
} else {
|
|
5307
5313
|
await this.#queueSteer(expandedText, options?.images);
|
|
5308
5314
|
}
|
|
@@ -5420,7 +5426,7 @@ export class AgentSession {
|
|
|
5420
5426
|
|
|
5421
5427
|
async promptCustomMessage<T = unknown>(
|
|
5422
5428
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
5423
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
|
|
5429
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
|
|
5424
5430
|
): Promise<void> {
|
|
5425
5431
|
const textContent =
|
|
5426
5432
|
typeof message.content === "string"
|
|
@@ -5434,7 +5440,10 @@ export class AgentSession {
|
|
|
5434
5440
|
if (!options?.streamingBehavior) {
|
|
5435
5441
|
throw new AgentBusyError();
|
|
5436
5442
|
}
|
|
5437
|
-
await this.sendCustomMessage(message, {
|
|
5443
|
+
await this.sendCustomMessage(message, {
|
|
5444
|
+
deliverAs: options.streamingBehavior,
|
|
5445
|
+
followUpQueuePolicy: options.followUpQueuePolicy,
|
|
5446
|
+
});
|
|
5438
5447
|
return;
|
|
5439
5448
|
}
|
|
5440
5449
|
|
|
@@ -5781,13 +5790,19 @@ export class AgentSession {
|
|
|
5781
5790
|
/**
|
|
5782
5791
|
* Queue a follow-up message to process after the agent would otherwise stop.
|
|
5783
5792
|
*/
|
|
5784
|
-
async followUp(
|
|
5793
|
+
async followUp(
|
|
5794
|
+
text: string,
|
|
5795
|
+
images?: ImageContent[],
|
|
5796
|
+
options?: Pick<PromptOptions, "followUpQueuePolicy">,
|
|
5797
|
+
): Promise<void> {
|
|
5785
5798
|
if (text.startsWith("/")) {
|
|
5786
5799
|
this.#throwIfExtensionCommand(text);
|
|
5787
5800
|
}
|
|
5788
5801
|
|
|
5789
5802
|
const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
5790
|
-
await this.#queueFollowUp(expandedText, images
|
|
5803
|
+
await this.#queueFollowUp(expandedText, images, {
|
|
5804
|
+
forceOneAtATime: options?.followUpQueuePolicy === "sequential",
|
|
5805
|
+
});
|
|
5791
5806
|
}
|
|
5792
5807
|
|
|
5793
5808
|
/**
|
|
@@ -5824,19 +5839,22 @@ export class AgentSession {
|
|
|
5824
5839
|
/**
|
|
5825
5840
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
5826
5841
|
*/
|
|
5827
|
-
async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
5842
|
+
async #queueFollowUp(text: string, images?: ImageContent[], options?: { forceOneAtATime?: boolean }): Promise<void> {
|
|
5828
5843
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
5829
5844
|
this.#followUpMessages.push(this.#createQueuedDisplayEntry(displayText));
|
|
5830
5845
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
5831
5846
|
if (images && images.length > 0) {
|
|
5832
5847
|
content.push(...images);
|
|
5833
5848
|
}
|
|
5834
|
-
this.agent.followUp(
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5849
|
+
this.agent.followUp(
|
|
5850
|
+
{
|
|
5851
|
+
role: "user",
|
|
5852
|
+
content,
|
|
5853
|
+
attribution: "user",
|
|
5854
|
+
timestamp: Date.now(),
|
|
5855
|
+
},
|
|
5856
|
+
options?.forceOneAtATime ? { forceOneAtATime: true } : undefined,
|
|
5857
|
+
);
|
|
5840
5858
|
// When fully idle AND the session is in a resumable assistant-ended state,
|
|
5841
5859
|
// schedule an immediate continue so the queued follow-up is delivered
|
|
5842
5860
|
// without waiting for the next user turn. We gate on isStreaming (model
|
|
@@ -5984,7 +6002,11 @@ export class AgentSession {
|
|
|
5984
6002
|
*/
|
|
5985
6003
|
async sendCustomMessage<T = unknown>(
|
|
5986
6004
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
5987
|
-
options?: {
|
|
6005
|
+
options?: {
|
|
6006
|
+
triggerTurn?: boolean;
|
|
6007
|
+
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
6008
|
+
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
6009
|
+
},
|
|
5988
6010
|
): Promise<void> {
|
|
5989
6011
|
const appMessage: CustomMessage<T> = {
|
|
5990
6012
|
role: "custom",
|
|
@@ -6002,7 +6024,10 @@ export class AgentSession {
|
|
|
6002
6024
|
}
|
|
6003
6025
|
|
|
6004
6026
|
if (options?.deliverAs === "followUp") {
|
|
6005
|
-
this.agent.followUp(
|
|
6027
|
+
this.agent.followUp(
|
|
6028
|
+
appMessage,
|
|
6029
|
+
options.followUpQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined,
|
|
6030
|
+
);
|
|
6006
6031
|
} else {
|
|
6007
6032
|
this.agent.steer(appMessage);
|
|
6008
6033
|
}
|
|
@@ -6928,7 +6953,7 @@ export class AgentSession {
|
|
|
6928
6953
|
|
|
6929
6954
|
/**
|
|
6930
6955
|
* Set thinking level.
|
|
6931
|
-
* Saves the effective metadata-clamped level to session and settings
|
|
6956
|
+
* Saves the effective metadata-clamped level to the session, and to settings when requested.
|
|
6932
6957
|
*/
|
|
6933
6958
|
setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
|
|
6934
6959
|
const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
|
|
@@ -6937,11 +6962,12 @@ export class AgentSession {
|
|
|
6937
6962
|
this.#thinkingLevel = effectiveLevel;
|
|
6938
6963
|
this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
|
|
6939
6964
|
|
|
6965
|
+
if (persist && effectiveLevel !== undefined) {
|
|
6966
|
+
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
6967
|
+
}
|
|
6968
|
+
|
|
6940
6969
|
if (isChanging) {
|
|
6941
6970
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
6942
|
-
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
6943
|
-
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
6944
|
-
}
|
|
6945
6971
|
this.#emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
|
|
6946
6972
|
}
|
|
6947
6973
|
}
|
|
@@ -115,6 +115,20 @@ function outputMentionsPsmux(output: string): boolean {
|
|
|
115
115
|
return PSMUX_VERSION_MARKERS.some(marker => output.includes(marker));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function normalizedCommandBaseName(command: string): string {
|
|
119
|
+
const normalized = command
|
|
120
|
+
.trim()
|
|
121
|
+
.replace(/^['"]|['"]$/g, "")
|
|
122
|
+
.replace(/\\/g, "/");
|
|
123
|
+
const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
124
|
+
return basename.endsWith(".exe") ? basename.slice(0, -4) : basename;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isNamedPsmuxCommand(command: string): boolean {
|
|
128
|
+
const basename = normalizedCommandBaseName(command);
|
|
129
|
+
return basename === "psmux" || basename === "pmux";
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function resolveBinaryPath(candidate: string): string | null {
|
|
119
133
|
return activeBinaryResolver(candidate);
|
|
120
134
|
}
|
|
@@ -194,13 +208,14 @@ export function resolveSkcTmuxBinary(options: ResolveSkcTmuxBinaryOptions = {}):
|
|
|
194
208
|
const runner = options.runner ?? readSpawnRunner();
|
|
195
209
|
const explicit = env.SKC_TMUX_COMMAND?.trim() || env.SKC_TEAM_TMUX_COMMAND?.trim();
|
|
196
210
|
if (explicit) {
|
|
197
|
-
const isPsmux =
|
|
211
|
+
const isPsmux =
|
|
212
|
+
platform === "win32" && isNamedPsmuxCommand(explicit) ? true : detectPsmux(explicit, { env, runner });
|
|
198
213
|
return { command: explicit, isPsmux, viaExplicitOverride: true };
|
|
199
214
|
}
|
|
200
215
|
if (platform === "win32") {
|
|
201
216
|
for (const candidate of PSMUX_BINARY_NAMES) {
|
|
202
217
|
if (resolveBinaryPath(candidate)) {
|
|
203
|
-
const isPsmux = detectPsmux(candidate, { env, runner });
|
|
218
|
+
const isPsmux = isNamedPsmuxCommand(candidate) ? true : detectPsmux(candidate, { env, runner });
|
|
204
219
|
return { command: candidate, isPsmux, viaExplicitOverride: false };
|
|
205
220
|
}
|
|
206
221
|
}
|
|
@@ -4,6 +4,7 @@ import { resolveSkcTmuxBinary } from "./psmux-detect";
|
|
|
4
4
|
export const SKC_DEFAULT_TMUX_SESSION = "sayknow_cli";
|
|
5
5
|
export const SKC_TMUX_SESSION_PREFIX = `${SKC_DEFAULT_TMUX_SESSION}_`;
|
|
6
6
|
export const SKC_TMUX_COMMAND_ENV = "SKC_TMUX_COMMAND";
|
|
7
|
+
export const SKC_TMUX_ACTIVE_SESSION_ENV = "SKC_TMUX_ACTIVE_SESSION";
|
|
7
8
|
export const SKC_TMUX_PROFILE_ENV = "SKC_TMUX_PROFILE";
|
|
8
9
|
export const SKC_TMUX_MOUSE_ENV = "SKC_MOUSE";
|
|
9
10
|
export const SKC_TMUX_PROFILE_OPTION = "@skc-profile";
|
|
@@ -4544,6 +4544,15 @@ function parseGitNameStatus(output: string): UltragoalChangeSetPath[] {
|
|
|
4544
4544
|
return rows;
|
|
4545
4545
|
}
|
|
4546
4546
|
|
|
4547
|
+
function categorizeCiChangedPath(value: string): UltragoalChangeCategory {
|
|
4548
|
+
// CI_DEV_CHANGED_PATHS intentionally carries path names only. Mixed registries
|
|
4549
|
+
// such as settings-schema.ts require diff-level narrowing; without the diff,
|
|
4550
|
+
// treating the whole registry as computer-control source forces the mandatory
|
|
4551
|
+
// computer red-team suite on unrelated settings changes.
|
|
4552
|
+
if (isSettingsSchemaPath(value)) return "other";
|
|
4553
|
+
return categorizeComputerChangePath(value);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4547
4556
|
function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
|
|
4548
4557
|
const raw = process.env.CI_DEV_CHANGED_PATHS;
|
|
4549
4558
|
if (!raw) return [];
|
|
@@ -4554,7 +4563,7 @@ function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
|
|
|
4554
4563
|
.map(pathValue => ({
|
|
4555
4564
|
path: normalizeRepoPath(pathValue),
|
|
4556
4565
|
status: "unknown" as UltragoalChangeStatus,
|
|
4557
|
-
category:
|
|
4566
|
+
category: categorizeCiChangedPath(pathValue),
|
|
4558
4567
|
}));
|
|
4559
4568
|
}
|
|
4560
4569
|
|