@sayknow-cli/coding-agent 0.5.25 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/dist/types/config/settings-schema.d.ts +51 -5
- package/dist/types/config/task-model-specialties.d.ts +55 -0
- package/dist/types/decisions/keyword-learning.d.ts +61 -0
- package/dist/types/decisions/llm-backend.d.ts +13 -1
- package/dist/types/decisions/prompt-triage.d.ts +42 -0
- package/dist/types/decisions/skill-routing.d.ts +41 -6
- package/dist/types/decisions/task-routing.d.ts +96 -11
- package/dist/types/hooks/native-prompt-routing.d.ts +21 -0
- package/dist/types/hooks/native-skill-hook.d.ts +3 -0
- package/dist/types/hooks/skill-keywords.d.ts +9 -0
- package/dist/types/hooks/skill-state.d.ts +20 -3
- package/dist/types/hooks/ui-skill-keywords.d.ts +15 -0
- package/dist/types/i18n/messages/en.d.ts +15 -0
- package/dist/types/lsp/index.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +3 -13
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/auth-storage-discovery.d.ts +13 -0
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +114 -18
- package/dist/types/tools/browser.d.ts +2 -2
- package/dist/types/tools/subagent.d.ts +2 -2
- package/package.json +7 -7
- package/scripts/eval-skill-routing.ts +37 -12
- package/src/config/settings-schema.ts +64 -12
- package/src/config/task-model-specialties.ts +131 -0
- package/src/decisions/index.ts +8 -2
- package/src/decisions/keyword-learning.ts +678 -0
- package/src/decisions/llm-backend.ts +213 -67
- package/src/decisions/prompt-triage.ts +163 -0
- package/src/decisions/skill-routing.ts +39 -56
- package/src/decisions/task-routing.ts +382 -66
- package/src/decisions/typesafe-backend.ts +3 -0
- package/src/hooks/native-prompt-routing.ts +190 -0
- package/src/hooks/native-skill-hook.ts +21 -12
- package/src/hooks/skill-keywords.ts +9 -0
- package/src/hooks/skill-state.ts +41 -10
- package/src/hooks/ui-skill-keywords.ts +67 -10
- package/src/i18n/messages/de.ts +16 -0
- package/src/i18n/messages/en.ts +16 -0
- package/src/i18n/messages/es.ts +16 -0
- package/src/i18n/messages/fr.ts +16 -0
- package/src/i18n/messages/ja.ts +16 -0
- package/src/i18n/messages/ko.ts +16 -0
- package/src/i18n/messages/zh.ts +16 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/components/model-selector.ts +275 -34
- package/src/modes/controllers/selector-controller.ts +50 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +1 -1
- package/src/prompts/tools/task.md +1 -0
- package/src/sdk/session.ts +5 -82
- package/src/session/agent-session.ts +137 -37
- package/src/session/auth-storage-discovery.ts +83 -0
- package/src/slash-commands/builtin-registry.ts +11 -9
- package/src/task/index.ts +98 -40
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +44 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt routing for the Codex host: the same three stages the session runs in
|
|
3
|
+
* `AgentSession#routeWorkflowSemantically`, rebuilt for a process that lives for
|
|
4
|
+
* one prompt.
|
|
5
|
+
*
|
|
6
|
+
* The session holds settings, credentials and a model registry for its whole
|
|
7
|
+
* life, so its model stage is a few lines. The hook is spawned fresh on every
|
|
8
|
+
* `UserPromptSubmit`, already costs ~260ms to start, and answers most prompts
|
|
9
|
+
* from the keyword tables alone — so everything the model stage needs is loaded
|
|
10
|
+
* lazily, and only on a prompt that actually reaches it. Concretely:
|
|
11
|
+
*
|
|
12
|
+
* - `decisions.enabled` / `decisions.keywordLearning` are read from the same raw
|
|
13
|
+
* `config.yml` files the hook already parses for skill discovery. Loading
|
|
14
|
+
* `Settings` for two booleans costs ~200ms of module graph on every prompt.
|
|
15
|
+
* - Learned keywords are one small JSON read, so they are always consulted: a
|
|
16
|
+
* pattern promoted in a CLI session must fire under Codex too.
|
|
17
|
+
* - Credentials, the registry and the decision service are opened only when the
|
|
18
|
+
* keyword tables did not fully answer, then closed before the hook exits.
|
|
19
|
+
*
|
|
20
|
+
* Every failure resolves to "no activation", which is what the hook did before
|
|
21
|
+
* this stage existed.
|
|
22
|
+
*/
|
|
23
|
+
import { getAgentDir } from "@sayknow-cli/utils";
|
|
24
|
+
import { YAML } from "bun";
|
|
25
|
+
import { loadLearnedKeywordDefinitions, observeRouting } from "../decisions/keyword-learning";
|
|
26
|
+
import type { PromptTriage, PromptTriageRequest, PromptTriager } from "../decisions/prompt-triage";
|
|
27
|
+
import type { SkillKeywordDefinition } from "./skill-keywords";
|
|
28
|
+
import { recordSkillActivation, type SkillActiveState } from "./skill-state";
|
|
29
|
+
import { buildUiSkillDirectiveForSkill, detectUiSkillKeywords } from "./ui-skill-keywords";
|
|
30
|
+
|
|
31
|
+
export interface NativePromptRoutingInput {
|
|
32
|
+
cwd: string;
|
|
33
|
+
text: string;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
threadId?: string;
|
|
36
|
+
turnId?: string;
|
|
37
|
+
stateDir?: string;
|
|
38
|
+
/** `config.yml` files in precedence order, lowest first; resolved by the hook. */
|
|
39
|
+
configPaths: readonly string[];
|
|
40
|
+
agentDir?: string;
|
|
41
|
+
/** Injected in tests so routing is exercised without credentials or a network. */
|
|
42
|
+
triager?: PromptTriager;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NativePromptRoutingResult {
|
|
46
|
+
skillState: SkillActiveState | null;
|
|
47
|
+
/** Directive for a UI skill the model picked because the pattern table missed. */
|
|
48
|
+
uiSkillContext: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DecisionSettings {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
keywordLearning: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The two decision switches, read the way the hook reads `skills.*`: raw YAML,
|
|
58
|
+
* later files override earlier ones, absent means the schema default (`true`).
|
|
59
|
+
*/
|
|
60
|
+
async function readDecisionSettings(configPaths: readonly string[]): Promise<DecisionSettings> {
|
|
61
|
+
const settings: DecisionSettings = { enabled: true, keywordLearning: true };
|
|
62
|
+
for (const configPath of configPaths) {
|
|
63
|
+
let raw: unknown;
|
|
64
|
+
try {
|
|
65
|
+
raw = YAML.parse(await Bun.file(configPath).text());
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
70
|
+
const decisions = (raw as Record<string, unknown>).decisions;
|
|
71
|
+
if (!decisions || typeof decisions !== "object" || Array.isArray(decisions)) continue;
|
|
72
|
+
const { enabled, keywordLearning } = decisions as Record<string, unknown>;
|
|
73
|
+
if (typeof enabled === "boolean") settings.enabled = enabled;
|
|
74
|
+
if (typeof keywordLearning === "boolean") settings.keywordLearning = keywordLearning;
|
|
75
|
+
}
|
|
76
|
+
return settings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Open credentials and the registry, build the triager, and hand back the
|
|
81
|
+
* teardown. Imported lazily: this graph is what the fast path must never pay for.
|
|
82
|
+
*/
|
|
83
|
+
async function openTriager(input: NativePromptRoutingInput): Promise<{ triager: PromptTriager; close: () => void }> {
|
|
84
|
+
const [{ Settings }, { ModelRegistry }, { discoverAuthStorage }, { createDecisionService }, { createPromptTriage }] =
|
|
85
|
+
await Promise.all([
|
|
86
|
+
import("../config/settings"),
|
|
87
|
+
import("../config/model-registry"),
|
|
88
|
+
import("../session/auth-storage-discovery"),
|
|
89
|
+
import("../decisions"),
|
|
90
|
+
import("../decisions/prompt-triage"),
|
|
91
|
+
]);
|
|
92
|
+
const agentDir = input.agentDir ?? getAgentDir();
|
|
93
|
+
const settings = await Settings.loadForScope({ cwd: input.cwd, agentDir });
|
|
94
|
+
const authStorage = await discoverAuthStorage(agentDir);
|
|
95
|
+
try {
|
|
96
|
+
const registry = new ModelRegistry(authStorage);
|
|
97
|
+
registry.applyConfiguredModelBindings(settings);
|
|
98
|
+
// Offline: built-in providers are static, and a local runtime's model list
|
|
99
|
+
// comes from the 24h discovery cache the CLI maintains. A network refresh here
|
|
100
|
+
// would put an unbounded fetch in front of every Codex prompt.
|
|
101
|
+
await registry.refresh("offline");
|
|
102
|
+
const service = createDecisionService({
|
|
103
|
+
registry,
|
|
104
|
+
settings,
|
|
105
|
+
sessionId: input.sessionId,
|
|
106
|
+
// This hook only runs under the Codex host, so the provider the user is
|
|
107
|
+
// already talking to is Codex; its small model ranks first when the user
|
|
108
|
+
// has that credential, and the hint is inert when they do not.
|
|
109
|
+
preferredProvider: "openai-codex",
|
|
110
|
+
enabled: true,
|
|
111
|
+
});
|
|
112
|
+
return { triager: createPromptTriage(service), close: () => authStorage.close() };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
authStorage.close();
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function routeNativePrompt(input: NativePromptRoutingInput): Promise<NativePromptRoutingResult> {
|
|
120
|
+
const decisions = await readDecisionSettings(input.configPaths);
|
|
121
|
+
const learned = await loadLearned(decisions.keywordLearning);
|
|
122
|
+
const uiMatched = detectUiSkillKeywords(input.text).length > 0;
|
|
123
|
+
|
|
124
|
+
let opened: { triager: PromptTriager; close: () => void } | undefined;
|
|
125
|
+
const triage = async (request: Omit<PromptTriageRequest, "text">): Promise<PromptTriage | null> => {
|
|
126
|
+
if (input.triager) return await input.triager({ text: input.text, ...request });
|
|
127
|
+
opened ??= await openTriager(input);
|
|
128
|
+
return await opened.triager({ text: input.text, ...request });
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Written from inside the semantic callback; a plain `let` would be narrowed
|
|
132
|
+
// to its initialiser by control-flow analysis and read as never-assigned here.
|
|
133
|
+
const seen: { semanticConsulted: boolean; result: PromptTriage | null } = { semanticConsulted: false, result: null };
|
|
134
|
+
try {
|
|
135
|
+
const skillState = await recordSkillActivation({
|
|
136
|
+
cwd: input.cwd,
|
|
137
|
+
text: input.text,
|
|
138
|
+
sessionId: input.sessionId,
|
|
139
|
+
threadId: input.threadId,
|
|
140
|
+
turnId: input.turnId,
|
|
141
|
+
stateDir: input.stateDir,
|
|
142
|
+
learned,
|
|
143
|
+
// Consulted only when no keyword matched — the same turn the session
|
|
144
|
+
// would spend a model call on, asking both questions at once.
|
|
145
|
+
resolveSkillSemantically: decisions.enabled
|
|
146
|
+
? async () => {
|
|
147
|
+
seen.semanticConsulted = true;
|
|
148
|
+
const result = await triage({ skipUiSkill: uiMatched });
|
|
149
|
+
seen.result = result;
|
|
150
|
+
// A null result is "no information" and teaches nothing; a result
|
|
151
|
+
// with a null workflow is the model saying "none", which is a
|
|
152
|
+
// negative example the learner needs.
|
|
153
|
+
if (decisions.keywordLearning && result) {
|
|
154
|
+
await observeRouting({
|
|
155
|
+
text: input.text,
|
|
156
|
+
skill: result.workflow,
|
|
157
|
+
confidence: result.workflowConfidence,
|
|
158
|
+
calibrated: result.calibrated,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return result?.workflow ?? null;
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
});
|
|
165
|
+
// A keyword answered the workflow question but the UI pattern table missed:
|
|
166
|
+
// the session still asks the UI question alone, and so does the hook.
|
|
167
|
+
if (decisions.enabled && !seen.semanticConsulted && !uiMatched) {
|
|
168
|
+
try {
|
|
169
|
+
seen.result = await triage({ skipWorkflow: true });
|
|
170
|
+
} catch {
|
|
171
|
+
seen.result = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
skillState,
|
|
176
|
+
uiSkillContext: seen.result?.uiSkill ? buildUiSkillDirectiveForSkill(seen.result.uiSkill) : null,
|
|
177
|
+
};
|
|
178
|
+
} finally {
|
|
179
|
+
opened?.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function loadLearned(enabled: boolean): Promise<SkillKeywordDefinition[]> {
|
|
184
|
+
if (!enabled) return [];
|
|
185
|
+
try {
|
|
186
|
+
return await loadLearnedKeywordDefinitions();
|
|
187
|
+
} catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -5,7 +5,9 @@ import { getAgentDir, getConfigDirName } from "@sayknow-cli/utils";
|
|
|
5
5
|
import { YAML } from "bun";
|
|
6
6
|
import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
|
|
7
7
|
import { DEFAULT_DISABLED_EXTENSIONS, DEFAULT_SKILL_DISCOVERY_SETTINGS } from "../config/skill-settings-defaults";
|
|
8
|
+
import type { PromptTriager } from "../decisions/prompt-triage";
|
|
8
9
|
import { sessionLogsDir } from "../skc-runtime/session-layout";
|
|
10
|
+
import { routeNativePrompt } from "./native-prompt-routing";
|
|
9
11
|
import {
|
|
10
12
|
buildActiveUltragoalPromptContext,
|
|
11
13
|
buildSkillActivationAdditionalContext,
|
|
@@ -13,7 +15,6 @@ import {
|
|
|
13
15
|
buildStateRecoveryDiagnosticsContext,
|
|
14
16
|
collectUserPromptStateRecoveryDiagnostics,
|
|
15
17
|
type EffectiveSkillConfigInput,
|
|
16
|
-
recordSkillActivation,
|
|
17
18
|
} from "./skill-state";
|
|
18
19
|
import { buildExternalUiSkillContext, buildUiSkillActivationContext } from "./ui-skill-keywords";
|
|
19
20
|
|
|
@@ -32,6 +33,8 @@ interface SkcNativeHookDispatchOptions {
|
|
|
32
33
|
stateDir?: string;
|
|
33
34
|
effectiveSkillConfig?: EffectiveSkillConfigInput;
|
|
34
35
|
configPaths?: string[];
|
|
36
|
+
/** Injected in tests so the model stage runs without credentials or a network. */
|
|
37
|
+
triager?: PromptTriager;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
interface ConfigCacheEntry {
|
|
@@ -337,19 +340,25 @@ async function dispatchSkcNativeSkillHookInner(
|
|
|
337
340
|
}
|
|
338
341
|
|
|
339
342
|
let skillState = null;
|
|
343
|
+
let semanticUiContext: string | null = null;
|
|
340
344
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
345
|
+
if (prompt) {
|
|
346
|
+
const routed = await routeNativePrompt({
|
|
347
|
+
cwd,
|
|
348
|
+
text: prompt,
|
|
349
|
+
sessionId: readSessionId(payload),
|
|
350
|
+
threadId: readThreadId(payload),
|
|
351
|
+
turnId: readTurnId(payload),
|
|
352
|
+
stateDir: options.stateDir,
|
|
353
|
+
configPaths: resolveConfigPaths(cwd, options.configPaths),
|
|
354
|
+
triager: options.triager,
|
|
355
|
+
});
|
|
356
|
+
skillState = routed.skillState;
|
|
357
|
+
semanticUiContext = routed.uiSkillContext;
|
|
358
|
+
}
|
|
351
359
|
} catch {
|
|
352
360
|
skillState = null;
|
|
361
|
+
semanticUiContext = null;
|
|
353
362
|
}
|
|
354
363
|
|
|
355
364
|
let effectiveSkillConfig: EffectiveSkillConfigInput | undefined;
|
|
@@ -395,7 +404,7 @@ async function dispatchSkcNativeSkillHookInner(
|
|
|
395
404
|
}
|
|
396
405
|
const additionalContext = [
|
|
397
406
|
skillState ? buildSkillActivationAdditionalContext(skillState, effectiveSkillConfig) : activeUltragoalContext,
|
|
398
|
-
uiSkillContext,
|
|
407
|
+
uiSkillContext ?? semanticUiContext,
|
|
399
408
|
externalUiContext,
|
|
400
409
|
recoveryContext,
|
|
401
410
|
classifyQuestionOnlyPrompt(prompt),
|
|
@@ -5,6 +5,15 @@ export interface SkillKeywordDefinition {
|
|
|
5
5
|
skill: SkcWorkflowSkill;
|
|
6
6
|
priority: number;
|
|
7
7
|
guidance: string;
|
|
8
|
+
/**
|
|
9
|
+
* Pre-compiled matcher, replacing the literal-substring compilation of
|
|
10
|
+
* `keyword`. Only learned entries set it: they are two stems with a bounded
|
|
11
|
+
* gap, which a literal string cannot express. `keyword` stays human-readable
|
|
12
|
+
* for logs and for the guidance line.
|
|
13
|
+
*/
|
|
14
|
+
pattern?: RegExp;
|
|
15
|
+
/** Mined from routing answers rather than written by hand. */
|
|
16
|
+
learned?: boolean;
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
export const SKC_WORKFLOW_SKILLS = CANONICAL_SKC_WORKFLOW_SKILLS;
|
package/src/hooks/skill-state.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
isSkcWorkflowSkill,
|
|
33
33
|
SKC_SKILL_KEYWORD_DEFINITIONS,
|
|
34
34
|
type SkcWorkflowSkill,
|
|
35
|
+
type SkillKeywordDefinition,
|
|
35
36
|
} from "./skill-keywords";
|
|
36
37
|
|
|
37
38
|
export const SKC_STATE_DIR = ".skc";
|
|
@@ -95,6 +96,8 @@ export interface SkillKeywordMatch {
|
|
|
95
96
|
keyword: string;
|
|
96
97
|
skill: SkcWorkflowSkill;
|
|
97
98
|
priority: number;
|
|
99
|
+
/** Matched a pattern mined from routing answers, not a hand-written keyword. */
|
|
100
|
+
learned?: boolean;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
export type { SkillActiveEntry, SkillActiveState } from "../skill-state/active-state";
|
|
@@ -121,6 +124,12 @@ export interface RecordSkillActivationInput {
|
|
|
121
124
|
turnId?: string;
|
|
122
125
|
nowIso?: string;
|
|
123
126
|
stateDir?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Patterns promoted by the keyword learner, appended after the hand-written
|
|
129
|
+
* table. The caller loads them because it knows whether learning is on; see
|
|
130
|
+
* `detectSkillKeywords`.
|
|
131
|
+
*/
|
|
132
|
+
learned?: readonly SkillKeywordDefinition[];
|
|
124
133
|
/**
|
|
125
134
|
* Semantic fallback, consulted only when no keyword matched. Supplying it turns the
|
|
126
135
|
* literal keyword table into a two-stage router; omitting it keeps the historical
|
|
@@ -161,10 +170,11 @@ function keywordToPattern(keyword: string): RegExp {
|
|
|
161
170
|
return new RegExp(`${prefix}${escaped}${suffix}`, "i");
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
|
|
165
|
-
...definition,
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
function compileDefinition(definition: SkillKeywordDefinition): SkillKeywordDefinition & { pattern: RegExp } {
|
|
174
|
+
return { ...definition, pattern: definition.pattern ?? keywordToPattern(definition.keyword) };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const KEYWORD_PATTERNS = SKC_SKILL_KEYWORD_DEFINITIONS.map(compileDefinition);
|
|
168
178
|
|
|
169
179
|
function parseExplicitSkillInvocations(text: string): {
|
|
170
180
|
matches: SkillKeywordMatch[];
|
|
@@ -192,16 +202,34 @@ function parseExplicitSkillInvocations(text: string): {
|
|
|
192
202
|
return { matches, sawExplicitLikeInvocation };
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Match a prompt against the keyword table.
|
|
207
|
+
*
|
|
208
|
+
* `learned` carries patterns mined from semantic routing answers (see
|
|
209
|
+
* `decisions/keyword-learning.ts`). It is a parameter rather than a module-level
|
|
210
|
+
* load because this file is imported by the hook process, where a synchronous
|
|
211
|
+
* disk read on every prompt is not acceptable and the caller already knows
|
|
212
|
+
* whether learning is enabled.
|
|
213
|
+
*/
|
|
214
|
+
export function detectSkillKeywords(
|
|
215
|
+
text: string,
|
|
216
|
+
learned: readonly SkillKeywordDefinition[] = [],
|
|
217
|
+
): SkillKeywordMatch[] {
|
|
196
218
|
const explicit = parseExplicitSkillInvocations(text);
|
|
197
219
|
if (explicit.matches.length > 0) return explicit.matches;
|
|
198
220
|
if (explicit.sawExplicitLikeInvocation) return [];
|
|
199
221
|
|
|
200
222
|
const implicit: SkillKeywordMatch[] = [];
|
|
201
|
-
|
|
223
|
+
const definitions = learned.length > 0 ? [...KEYWORD_PATTERNS, ...learned.map(compileDefinition)] : KEYWORD_PATTERNS;
|
|
224
|
+
for (const definition of definitions) {
|
|
202
225
|
const match = text.match(definition.pattern);
|
|
203
226
|
if (!match) continue;
|
|
204
|
-
implicit.push({
|
|
227
|
+
implicit.push({
|
|
228
|
+
keyword: match[0],
|
|
229
|
+
skill: definition.skill,
|
|
230
|
+
priority: definition.priority,
|
|
231
|
+
...(definition.learned ? { learned: true } : {}),
|
|
232
|
+
});
|
|
205
233
|
}
|
|
206
234
|
|
|
207
235
|
const merged: SkillKeywordMatch[] = [];
|
|
@@ -212,8 +240,11 @@ export function detectSkillKeywords(text: string): SkillKeywordMatch[] {
|
|
|
212
240
|
return merged;
|
|
213
241
|
}
|
|
214
242
|
|
|
215
|
-
export function detectPrimarySkillKeyword(
|
|
216
|
-
|
|
243
|
+
export function detectPrimarySkillKeyword(
|
|
244
|
+
text: string,
|
|
245
|
+
learned: readonly SkillKeywordDefinition[] = [],
|
|
246
|
+
): SkillKeywordMatch | null {
|
|
247
|
+
return detectSkillKeywords(text, learned)[0] ?? null;
|
|
217
248
|
}
|
|
218
249
|
|
|
219
250
|
export function resolveSkcStateDir(cwd: string, stateDir?: string): string {
|
|
@@ -476,7 +507,7 @@ async function seedSkillActivationState(
|
|
|
476
507
|
// Fallback for native-hook prompts when SkillPromptDetails.subskillActivation is absent;
|
|
477
508
|
// real /skill dispatch paths resolve sub-skill activation before prompt construction.
|
|
478
509
|
export async function recordSkillActivation(input: RecordSkillActivationInput): Promise<SkillActiveState | null> {
|
|
479
|
-
const match = detectPrimarySkillKeyword(input.text);
|
|
510
|
+
const match = detectPrimarySkillKeyword(input.text, input.learned);
|
|
480
511
|
if (match) return await seedSkillActivationState(match.skill, match.keyword, "skc-skill-state-hook", input);
|
|
481
512
|
if (!input.resolveSkillSemantically) return null;
|
|
482
513
|
// The semantic stage is advisory: any failure leaves routing to the system prompt,
|
|
@@ -160,9 +160,29 @@ export function detectUiSkillKeywords(text: string): UiSkillKeywordMatch[] {
|
|
|
160
160
|
}
|
|
161
161
|
// Keep the lead skill plus the craft baseline; more than that is noise.
|
|
162
162
|
const lead = deduped[0]!;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
return pairWithBaseline(lead.skill);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A specialized skill plus the craft baseline, which is the pairing the skills
|
|
168
|
+
* themselves assume. Skills that carry their own complete craft bar stand alone.
|
|
169
|
+
*/
|
|
170
|
+
function pairWithBaseline(skill: BundledSkcUiSkillName): UiSkillKeywordMatch[] {
|
|
171
|
+
const definition = UI_SKILL_KEYWORD_DEFINITIONS.find(item => item.skill === skill);
|
|
172
|
+
const lead: UiSkillKeywordMatch = { skill, keyword: skill, priority: definition?.priority ?? 0 };
|
|
173
|
+
if (skill === UI_SKILL_BASE || SELF_SUFFICIENT_UI_SKILLS.has(skill)) return [lead];
|
|
174
|
+
const base = UI_SKILL_KEYWORD_DEFINITIONS.find(item => item.skill === UI_SKILL_BASE);
|
|
175
|
+
return [lead, { skill: UI_SKILL_BASE, keyword: UI_SKILL_BASE, priority: base?.priority ?? 0 }];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderUiSkillDirective(skills: readonly BundledSkcUiSkillName[], evidence: string): string {
|
|
179
|
+
const names = skills.map(skill => `\`${skill}\``).join(" then ");
|
|
180
|
+
return [
|
|
181
|
+
`SKC detected frontend UI/UX work in this prompt (${evidence}).`,
|
|
182
|
+
`Before writing or reviewing that surface, load ${names} with the \`skill\` tool.`,
|
|
183
|
+
"These skills are bundled with SKC: never tell the user to install them, and never skip them because the task looks small.",
|
|
184
|
+
"They are not workflow skills and have no `skc state` mode state.",
|
|
185
|
+
].join(" ");
|
|
166
186
|
}
|
|
167
187
|
|
|
168
188
|
/**
|
|
@@ -173,15 +193,52 @@ export function detectUiSkillKeywords(text: string): UiSkillKeywordMatch[] {
|
|
|
173
193
|
export function buildUiSkillActivationContext(text: string): string | null {
|
|
174
194
|
const matches = detectUiSkillKeywords(text);
|
|
175
195
|
if (matches.length === 0) return null;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
"These skills are bundled with SKC: never tell the user to install them, and never skip them because the task looks small.",
|
|
181
|
-
"They are not workflow skills and have no `skc state` mode state.",
|
|
182
|
-
].join(" ");
|
|
196
|
+
return renderUiSkillDirective(
|
|
197
|
+
matches.map(match => match.skill),
|
|
198
|
+
`matched "${matches[0]!.keyword}"`,
|
|
199
|
+
);
|
|
183
200
|
}
|
|
184
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Same directive, for a skill chosen by the semantic stage rather than by a
|
|
204
|
+
* pattern. The regex table caught 5 of 8 real frontend prompts; this is the path
|
|
205
|
+
* for the other three, and it names its source so a wrong pick is traceable to
|
|
206
|
+
* the model rather than to a pattern nobody can find.
|
|
207
|
+
*/
|
|
208
|
+
export function buildUiSkillDirectiveForSkill(skill: BundledSkcUiSkillName): string {
|
|
209
|
+
return renderUiSkillDirective(
|
|
210
|
+
pairWithBaseline(skill).map(match => match.skill),
|
|
211
|
+
"semantic match",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* What each bundled skill is *for*, in the words a user would recognise.
|
|
217
|
+
*
|
|
218
|
+
* This is the whole contract with the routing model — the skill ids alone carry
|
|
219
|
+
* almost no signal, and `appllama-app-design-skill` carries actively misleading
|
|
220
|
+
* signal. Kept next to the patterns so the two cannot drift apart.
|
|
221
|
+
*/
|
|
222
|
+
export const BUNDLED_UI_SKILL_MEANINGS: Record<BundledSkcUiSkillName, string> = {
|
|
223
|
+
"emil-design-eng":
|
|
224
|
+
"General web UI/UX craft: building or reviewing components, layout, spacing, typography, design systems, CSS. The default for frontend work with no more specific fit.",
|
|
225
|
+
animate:
|
|
226
|
+
"Adding new motion to a web UI: animations, transitions, easing, keyframes, hover effects, micro-interactions.",
|
|
227
|
+
"review-animations": "Reviewing motion that already exists and judging whether it is good.",
|
|
228
|
+
"improve-animations": "Auditing and fixing motion that already exists and is known to be wrong or cheap-looking.",
|
|
229
|
+
"find-animation-opportunities":
|
|
230
|
+
"Asking where motion could be added to a surface that currently has none, without naming a specific effect.",
|
|
231
|
+
"pick-ui-library": "Choosing between UI libraries, component kits, or frontend packages.",
|
|
232
|
+
prototype: "Exploring several visual variants or mockups of the same screen before committing to one.",
|
|
233
|
+
"mobile-native":
|
|
234
|
+
"Making a web page on a phone feel native: touch targets, safe areas, viewport and scroll behaviour, gestures.",
|
|
235
|
+
"animation-vocabulary": "Naming a motion effect the user described but could not name.",
|
|
236
|
+
"apple-design": "Apple-style motion, materials, and Human Interface Guidelines feel.",
|
|
237
|
+
"ask-sonner": "Toast notifications, specifically the Sonner library.",
|
|
238
|
+
"appllama-app-design-skill": "Native app screens built with Expo or React Native — not a website viewed on a phone.",
|
|
239
|
+
"react-bits": "Pre-built animated React components: animated text, animated backgrounds, cursor and scroll effects.",
|
|
240
|
+
};
|
|
241
|
+
|
|
185
242
|
/**
|
|
186
243
|
* Frontend skills SKC routes to but deliberately does NOT vendor.
|
|
187
244
|
*
|
package/src/i18n/messages/de.ts
CHANGED
|
@@ -75,6 +75,22 @@ export const de: Partial<Record<MsgKey, string>> = {
|
|
|
75
75
|
"modelSelector.noMatching": "Keine passenden Modelle.",
|
|
76
76
|
"modelSelector.modelName": "Modellname: {value}",
|
|
77
77
|
"modelSelector.actionFor": "Aktion für: {id}",
|
|
78
|
+
"modelSelector.setAsTarget": "Als {tag} ({name}) festlegen",
|
|
79
|
+
"modelSelector.setForAllRoleAgents": "Fur alle Rollen-Agenten setzen",
|
|
80
|
+
"modelSelector.setForAllTargets": "Fur alle Ziele setzen",
|
|
81
|
+
"modelSelector.hasDetailedUses": "(detaillierte Aufgaben)",
|
|
82
|
+
"modelSelector.detailedUseFor": "Detaillierte Aufgabe fur {target}: {id}",
|
|
83
|
+
"modelSelector.generalRole": "Allgemein (ganze Rolle)",
|
|
84
|
+
"modelSelector.resetSpecialties": "Detaillierte Aufgaben zurucksetzen",
|
|
85
|
+
"modelSelector.specialty.backendArchitecture": "Backend-Architektur",
|
|
86
|
+
"modelSelector.specialty.frontendDesign": "Frontend-Design",
|
|
87
|
+
"modelSelector.specialty.implementation": "Implementierung",
|
|
88
|
+
"modelSelector.specialty.testing": "Tests",
|
|
89
|
+
"modelSelector.specialty.review": "Review",
|
|
90
|
+
"modelSelector.specialtySaved": "{specialty} verwendet {value}.",
|
|
91
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
92
|
+
"{specialty} verwendet {value}, sobald eine Aufgabe diese Arbeit deklariert. Automatische Erkennung aus dem Auftragstext bleibt aus, bis task.modelRouting.enabled aktiv ist.",
|
|
93
|
+
"modelSelector.specialtyCleared": "Detaillierte Aufgaben fur {target} zuruckgesetzt.",
|
|
78
94
|
"modelSelector.reasoningFor": "Reasoning für {target}: {id}",
|
|
79
95
|
"modelSelector.temporaryModel": "temporäres Modell",
|
|
80
96
|
};
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -95,6 +95,22 @@ export const en = {
|
|
|
95
95
|
"modelSelector.noMatching": "No matching models.",
|
|
96
96
|
"modelSelector.modelName": "Model Name: {value}",
|
|
97
97
|
"modelSelector.actionFor": "Action for: {id}",
|
|
98
|
+
"modelSelector.setAsTarget": "Set as {tag} ({name})",
|
|
99
|
+
"modelSelector.setForAllRoleAgents": "Set for all role agents",
|
|
100
|
+
"modelSelector.setForAllTargets": "Set for all targets",
|
|
101
|
+
"modelSelector.hasDetailedUses": "(detailed uses)",
|
|
102
|
+
"modelSelector.detailedUseFor": "Detailed use for {target}: {id}",
|
|
103
|
+
"modelSelector.generalRole": "General (whole role)",
|
|
104
|
+
"modelSelector.resetSpecialties": "Clear detailed-use overrides",
|
|
105
|
+
"modelSelector.specialty.backendArchitecture": "Backend architecture",
|
|
106
|
+
"modelSelector.specialty.frontendDesign": "Frontend design",
|
|
107
|
+
"modelSelector.specialty.implementation": "Implementation",
|
|
108
|
+
"modelSelector.specialty.testing": "Testing",
|
|
109
|
+
"modelSelector.specialty.review": "Review",
|
|
110
|
+
"modelSelector.specialtySaved": "{specialty} will use {value}.",
|
|
111
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
112
|
+
"{specialty} will use {value} whenever a task declares that work. Auto-detection from assignment text stays off until you enable task.modelRouting.enabled.",
|
|
113
|
+
"modelSelector.specialtyCleared": "Cleared detailed-use overrides for {target}.",
|
|
98
114
|
"modelSelector.reasoningFor": "Reasoning for {target}: {id}",
|
|
99
115
|
"modelSelector.temporaryModel": "temporary model",
|
|
100
116
|
} as const;
|
package/src/i18n/messages/es.ts
CHANGED
|
@@ -75,6 +75,22 @@ export const es: Partial<Record<MsgKey, string>> = {
|
|
|
75
75
|
"modelSelector.noMatching": "No hay modelos coincidentes.",
|
|
76
76
|
"modelSelector.modelName": "Nombre del modelo: {value}",
|
|
77
77
|
"modelSelector.actionFor": "Acción para: {id}",
|
|
78
|
+
"modelSelector.setAsTarget": "Asignar como {tag} ({name})",
|
|
79
|
+
"modelSelector.setForAllRoleAgents": "Asignar a todos los agentes de rol",
|
|
80
|
+
"modelSelector.setForAllTargets": "Asignar a todos los destinos",
|
|
81
|
+
"modelSelector.hasDetailedUses": "(usos detallados)",
|
|
82
|
+
"modelSelector.detailedUseFor": "Uso detallado de {target}: {id}",
|
|
83
|
+
"modelSelector.generalRole": "General (todo el rol)",
|
|
84
|
+
"modelSelector.resetSpecialties": "Borrar los usos detallados",
|
|
85
|
+
"modelSelector.specialty.backendArchitecture": "Arquitectura de backend",
|
|
86
|
+
"modelSelector.specialty.frontendDesign": "Diseño de frontend",
|
|
87
|
+
"modelSelector.specialty.implementation": "Implementación",
|
|
88
|
+
"modelSelector.specialty.testing": "Pruebas",
|
|
89
|
+
"modelSelector.specialty.review": "Revisión",
|
|
90
|
+
"modelSelector.specialtySaved": "{specialty} usará {value}.",
|
|
91
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
92
|
+
"{specialty} usará {value} cuando una tarea declare ese trabajo. La detección automática desde el texto de la tarea sigue desactivada hasta que actives task.modelRouting.enabled.",
|
|
93
|
+
"modelSelector.specialtyCleared": "Se borraron los usos detallados de {target}.",
|
|
78
94
|
"modelSelector.reasoningFor": "Razonamiento para {target}: {id}",
|
|
79
95
|
"modelSelector.temporaryModel": "modelo temporal",
|
|
80
96
|
};
|
package/src/i18n/messages/fr.ts
CHANGED
|
@@ -75,6 +75,22 @@ export const fr: Partial<Record<MsgKey, string>> = {
|
|
|
75
75
|
"modelSelector.noMatching": "Aucun modèle correspondant.",
|
|
76
76
|
"modelSelector.modelName": "Nom du modèle : {value}",
|
|
77
77
|
"modelSelector.actionFor": "Action pour : {id}",
|
|
78
|
+
"modelSelector.setAsTarget": "Définir comme {tag} ({name})",
|
|
79
|
+
"modelSelector.setForAllRoleAgents": "Definir pour tous les agents de role",
|
|
80
|
+
"modelSelector.setForAllTargets": "Definir pour toutes les cibles",
|
|
81
|
+
"modelSelector.hasDetailedUses": "(usages detailles)",
|
|
82
|
+
"modelSelector.detailedUseFor": "Usage detaille pour {target} : {id}",
|
|
83
|
+
"modelSelector.generalRole": "General (tout le role)",
|
|
84
|
+
"modelSelector.resetSpecialties": "Effacer les usages detailles",
|
|
85
|
+
"modelSelector.specialty.backendArchitecture": "Architecture backend",
|
|
86
|
+
"modelSelector.specialty.frontendDesign": "Design frontend",
|
|
87
|
+
"modelSelector.specialty.implementation": "Implementation",
|
|
88
|
+
"modelSelector.specialty.testing": "Tests",
|
|
89
|
+
"modelSelector.specialty.review": "Revue",
|
|
90
|
+
"modelSelector.specialtySaved": "{specialty} utilisera {value}.",
|
|
91
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
92
|
+
"{specialty} utilisera {value} des qu'une tache declare ce travail. La detection automatique depuis le texte reste inactive sans task.modelRouting.enabled.",
|
|
93
|
+
"modelSelector.specialtyCleared": "Usages detailles effaces pour {target}.",
|
|
78
94
|
"modelSelector.reasoningFor": "Raisonnement pour {target} : {id}",
|
|
79
95
|
"modelSelector.temporaryModel": "modèle temporaire",
|
|
80
96
|
};
|
package/src/i18n/messages/ja.ts
CHANGED
|
@@ -79,6 +79,22 @@ export const ja: Partial<Record<MsgKey, string>> = {
|
|
|
79
79
|
"modelSelector.noMatching": "一致するモデルがありません。",
|
|
80
80
|
"modelSelector.modelName": "モデル名: {value}",
|
|
81
81
|
"modelSelector.actionFor": "操作対象: {id}",
|
|
82
|
+
"modelSelector.setAsTarget": "{tag} に設定",
|
|
83
|
+
"modelSelector.setForAllRoleAgents": "すべてのロールエージェントに設定",
|
|
84
|
+
"modelSelector.setForAllTargets": "すべての対象に設定",
|
|
85
|
+
"modelSelector.hasDetailedUses": "(詳細用途あり)",
|
|
86
|
+
"modelSelector.detailedUseFor": "{target} の詳細用途: {id}",
|
|
87
|
+
"modelSelector.generalRole": "一般(ロール全体)",
|
|
88
|
+
"modelSelector.resetSpecialties": "詳細用途の設定を解除",
|
|
89
|
+
"modelSelector.specialty.backendArchitecture": "バックエンド設計",
|
|
90
|
+
"modelSelector.specialty.frontendDesign": "フロントエンドデザイン",
|
|
91
|
+
"modelSelector.specialty.implementation": "実装",
|
|
92
|
+
"modelSelector.specialty.testing": "テスト",
|
|
93
|
+
"modelSelector.specialty.review": "レビュー",
|
|
94
|
+
"modelSelector.specialtySaved": "{specialty} には {value} を使用します。",
|
|
95
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
96
|
+
"{specialty} として宣言されたタスクには {value} を使用します。指示文からの自動判定は task.modelRouting.enabled を有効にするまで無効です。",
|
|
97
|
+
"modelSelector.specialtyCleared": "{target} の詳細用途設定を解除しました。",
|
|
82
98
|
"modelSelector.reasoningFor": "{target} の推論レベル: {id}",
|
|
83
99
|
"modelSelector.temporaryModel": "一時モデル",
|
|
84
100
|
};
|
package/src/i18n/messages/ko.ts
CHANGED
|
@@ -79,6 +79,22 @@ export const ko: Partial<Record<MsgKey, string>> = {
|
|
|
79
79
|
"modelSelector.noMatching": "일치하는 모델이 없습니다.",
|
|
80
80
|
"modelSelector.modelName": "모델 이름: {value}",
|
|
81
81
|
"modelSelector.actionFor": "작업 대상: {id}",
|
|
82
|
+
"modelSelector.setAsTarget": "{tag}에 설정",
|
|
83
|
+
"modelSelector.setForAllRoleAgents": "모든 역할 에이전트에 설정",
|
|
84
|
+
"modelSelector.setForAllTargets": "모든 대상에 설정",
|
|
85
|
+
"modelSelector.hasDetailedUses": "(세부 업무 있음)",
|
|
86
|
+
"modelSelector.detailedUseFor": "{target} 세부 업무: {id}",
|
|
87
|
+
"modelSelector.generalRole": "일반 (역할 전체)",
|
|
88
|
+
"modelSelector.resetSpecialties": "세부 업무 설정 해제",
|
|
89
|
+
"modelSelector.specialty.backendArchitecture": "백엔드 아키텍처",
|
|
90
|
+
"modelSelector.specialty.frontendDesign": "프런트엔드 디자인",
|
|
91
|
+
"modelSelector.specialty.implementation": "구현",
|
|
92
|
+
"modelSelector.specialty.testing": "테스트",
|
|
93
|
+
"modelSelector.specialty.review": "리뷰",
|
|
94
|
+
"modelSelector.specialtySaved": "{specialty} 작업에 {value}을(를) 사용합니다.",
|
|
95
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
96
|
+
"{specialty} 작업으로 선언된 태스크는 {value}을(를) 사용합니다. 지시문에서 자동 감지하려면 task.modelRouting.enabled를 켜야 합니다.",
|
|
97
|
+
"modelSelector.specialtyCleared": "{target}의 세부 업무 설정을 해제했습니다.",
|
|
82
98
|
"modelSelector.reasoningFor": "{target} 추론 수준: {id}",
|
|
83
99
|
"modelSelector.temporaryModel": "임시 모델",
|
|
84
100
|
};
|
package/src/i18n/messages/zh.ts
CHANGED
|
@@ -79,6 +79,22 @@ export const zh: Partial<Record<MsgKey, string>> = {
|
|
|
79
79
|
"modelSelector.noMatching": "没有匹配的模型。",
|
|
80
80
|
"modelSelector.modelName": "模型名称:{value}",
|
|
81
81
|
"modelSelector.actionFor": "操作对象:{id}",
|
|
82
|
+
"modelSelector.setAsTarget": "设为 {tag}",
|
|
83
|
+
"modelSelector.setForAllRoleAgents": "设为所有角色代理",
|
|
84
|
+
"modelSelector.setForAllTargets": "设为所有对象",
|
|
85
|
+
"modelSelector.hasDetailedUses": "(含细分用途)",
|
|
86
|
+
"modelSelector.detailedUseFor": "{target} 的细分用途:{id}",
|
|
87
|
+
"modelSelector.generalRole": "通用(整个角色)",
|
|
88
|
+
"modelSelector.resetSpecialties": "清除细分用途设置",
|
|
89
|
+
"modelSelector.specialty.backendArchitecture": "后端架构",
|
|
90
|
+
"modelSelector.specialty.frontendDesign": "前端设计",
|
|
91
|
+
"modelSelector.specialty.implementation": "实现",
|
|
92
|
+
"modelSelector.specialty.testing": "测试",
|
|
93
|
+
"modelSelector.specialty.review": "评审",
|
|
94
|
+
"modelSelector.specialtySaved": "{specialty} 将使用 {value}。",
|
|
95
|
+
"modelSelector.specialtySavedRoutingOff":
|
|
96
|
+
"声明为 {specialty} 的任务将使用 {value}。若要从任务文本自动识别,需开启 task.modelRouting.enabled。",
|
|
97
|
+
"modelSelector.specialtyCleared": "已清除 {target} 的细分用途设置。",
|
|
82
98
|
"modelSelector.reasoningFor": "{target} 的推理级别:{id}",
|
|
83
99
|
"modelSelector.temporaryModel": "临时模型",
|
|
84
100
|
};
|