@sayknow-cli/coding-agent 0.3.1 → 0.3.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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +0 -5
- package/dist/types/config/settings-schema.d.ts +79 -68
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/i18n/messages/en.d.ts +0 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +0 -1
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +114 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +3 -2
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +6 -3
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +29 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +62 -8
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +3 -6
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +88 -103
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/hooks/skill-state.ts +57 -0
- package/src/i18n/messages/de.ts +0 -1
- package/src/i18n/messages/en.ts +0 -1
- package/src/i18n/messages/es.ts +0 -1
- package/src/i18n/messages/fr.ts +0 -1
- package/src/i18n/messages/ja.ts +0 -1
- package/src/i18n/messages/ko.ts +0 -1
- package/src/i18n/messages/zh.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -11
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +65 -12
- package/src/modes/components/settings-defs.ts +1 -2
- package/src/modes/components/settings-selector.ts +2 -7
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +59 -11
- package/src/modes/interactive-mode.ts +4 -1
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/theme.ts +0 -4
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +778 -257
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +162 -10
- package/src/session/agent-session.ts +210 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +68 -19
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/state-renderer.ts +13 -0
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +192 -8
- package/src/skc-runtime/ultragoal-runtime.ts +296 -16
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/fetch.ts +18 -2
- package/src/tools/index.ts +5 -9
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/url-guard.ts +18 -14
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { inflateSync } from "node:zlib";
|
|
4
|
-
|
|
5
|
+
import { normalizeGoal } from "../goals/state";
|
|
6
|
+
import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
|
|
5
7
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
6
8
|
import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
|
|
7
9
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
8
|
-
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
10
|
+
import { DEFAULT_ULTRAGOAL_OBJECTIVE, SKC_SESSION_FILE_ENV } from "./goal-mode-request";
|
|
9
11
|
import { sessionUltragoalDir, skcRoot } from "./session-layout";
|
|
10
12
|
import { resolveSkcSessionForRead, resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
11
13
|
import { renderUltragoalStatusMarkdown } from "./state-renderer";
|
|
12
14
|
import { reconcileWorkflowSkillState } from "./state-runtime";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
appendJsonl,
|
|
17
|
+
persistedStateRevision,
|
|
18
|
+
withWorkflowStateLock,
|
|
19
|
+
writeArtifact,
|
|
20
|
+
writeGuardedJsonAtomic,
|
|
21
|
+
} from "./state-writer";
|
|
14
22
|
|
|
15
23
|
export type UltragoalSkcGoalMode = "aggregate" | "per-story";
|
|
16
24
|
export type UltragoalGoalStatus =
|
|
@@ -79,6 +87,44 @@ export interface UltragoalLedgerEvent extends JsonObject {
|
|
|
79
87
|
timestamp?: string;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
export type UltragoalNudgeSurface = "pause" | "drop" | "ask" | "premature_complete";
|
|
91
|
+
export type UltragoalNudgeTargetKind = "story" | "final_aggregate_receipt";
|
|
92
|
+
|
|
93
|
+
export interface UltragoalNudgeLedgerEvent extends UltragoalLedgerEvent {
|
|
94
|
+
event: "nudge";
|
|
95
|
+
goalId: string;
|
|
96
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
97
|
+
surface: UltragoalNudgeSurface;
|
|
98
|
+
attempt: number;
|
|
99
|
+
budget: number;
|
|
100
|
+
reason: string;
|
|
101
|
+
currentGoalObjective?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface UltragoalNudgeTarget {
|
|
105
|
+
goalId: string;
|
|
106
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type UltragoalNudgeOutcome =
|
|
110
|
+
| {
|
|
111
|
+
nudged: true;
|
|
112
|
+
attempt: number;
|
|
113
|
+
budget: number;
|
|
114
|
+
goalId: string;
|
|
115
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
116
|
+
event: UltragoalNudgeLedgerEvent;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
nudged: false;
|
|
120
|
+
exhausted: true;
|
|
121
|
+
count: number;
|
|
122
|
+
budget: number;
|
|
123
|
+
goalId: string;
|
|
124
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
125
|
+
}
|
|
126
|
+
| { nudged: false; inactive: true; reason: string };
|
|
127
|
+
|
|
82
128
|
export interface UltragoalPaths {
|
|
83
129
|
dir: string;
|
|
84
130
|
briefPath: string;
|
|
@@ -94,6 +140,11 @@ export interface UltragoalStatusSummary {
|
|
|
94
140
|
currentGoal?: UltragoalGoal;
|
|
95
141
|
counts: Record<UltragoalGoalStatus, number>;
|
|
96
142
|
goals: UltragoalGoal[];
|
|
143
|
+
nudgeBudget?: number;
|
|
144
|
+
nudgeCount?: number;
|
|
145
|
+
nudgeRemaining?: number;
|
|
146
|
+
nudgeGoalId?: string;
|
|
147
|
+
nudgeTargetKind?: UltragoalNudgeTargetKind;
|
|
97
148
|
}
|
|
98
149
|
|
|
99
150
|
export interface UltragoalCommandResult {
|
|
@@ -219,6 +270,156 @@ export async function readUltragoalLedger(cwd: string, sessionId?: string | null
|
|
|
219
270
|
}
|
|
220
271
|
}
|
|
221
272
|
|
|
273
|
+
export const DEFAULT_ULTRAGOAL_NUDGE_BUDGET = 10;
|
|
274
|
+
|
|
275
|
+
/** Pure: count ledger `nudge` rows for an exact goalId. */
|
|
276
|
+
export function countUltragoalNudges(ledger: readonly UltragoalLedgerEvent[], goalId: string): number {
|
|
277
|
+
return ledger.filter(event => event.event === "nudge" && event.goalId === goalId).length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseNudgeBudgetValue(value: unknown): number | null {
|
|
281
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 ? value : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function readSettingsNudgeBudget(settingsPath: string): Promise<number | null> {
|
|
285
|
+
try {
|
|
286
|
+
const raw = await Bun.file(settingsPath).text();
|
|
287
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
288
|
+
// Support both the flat dotted key and a nested skc.ultragoal.nudgeBudget shape.
|
|
289
|
+
const flat = parseNudgeBudgetValue(parsed["skc.ultragoal.nudgeBudget"]);
|
|
290
|
+
if (flat !== null) return flat;
|
|
291
|
+
const skc = parsed.skc;
|
|
292
|
+
if (skc && typeof skc === "object") {
|
|
293
|
+
const ultragoal = (skc as Record<string, unknown>).ultragoal;
|
|
294
|
+
if (ultragoal && typeof ultragoal === "object") {
|
|
295
|
+
return parseNudgeBudgetValue((ultragoal as Record<string, unknown>).nudgeBudget);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve the per-story nudge budget. Project `./.skc/settings.json` overrides the
|
|
306
|
+
* user settings (`$SKC_CONFIG_DIR/settings.json` or `~/.skc/settings.json`), else the
|
|
307
|
+
* default. Mirrors the `skc.deepInterview.ambiguityThreshold` user+project precedence.
|
|
308
|
+
*/
|
|
309
|
+
export async function resolveUltragoalNudgeBudget(cwd: string): Promise<{ budget: number; source: string }> {
|
|
310
|
+
const projectPath = path.join(skcRoot(cwd), "settings.json");
|
|
311
|
+
const project = await readSettingsNudgeBudget(projectPath);
|
|
312
|
+
if (project !== null) return { budget: project, source: projectPath };
|
|
313
|
+
const userDir = process.env.SKC_CONFIG_DIR?.trim() || path.join(os.homedir(), ".skc");
|
|
314
|
+
const userPath = path.join(userDir, "settings.json");
|
|
315
|
+
const user = await readSettingsNudgeBudget(userPath);
|
|
316
|
+
if (user !== null) return { budget: user, source: userPath };
|
|
317
|
+
return { budget: DEFAULT_ULTRAGOAL_NUDGE_BUDGET, source: "default" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Pure canonical selector shared by guards and status so `nudgeGoalId` can never
|
|
322
|
+
* diverge between what a guard consumes and what status displays. Prefers the active
|
|
323
|
+
* current-goal objective, then active > pending > failed (matching `chooseNextGoal`),
|
|
324
|
+
* then the aggregate final-receipt target when all stories are complete but the
|
|
325
|
+
* aggregate run still needs a final receipt. Returns null for verified-complete or
|
|
326
|
+
* absent/unrelated plans.
|
|
327
|
+
*/
|
|
328
|
+
export function selectUltragoalNudgeTarget(
|
|
329
|
+
plan: UltragoalPlan,
|
|
330
|
+
options: { currentGoalObjective?: string; retryFailed?: boolean } = {},
|
|
331
|
+
): UltragoalNudgeTarget | null {
|
|
332
|
+
const objective = options.currentGoalObjective?.trim();
|
|
333
|
+
if (objective) {
|
|
334
|
+
const matched = plan.goals.find(
|
|
335
|
+
goal => goal.objective.trim() === objective && SCHEDULABLE_STATUSES.has(goal.status),
|
|
336
|
+
);
|
|
337
|
+
if (matched) return { goalId: matched.id, targetKind: "story" };
|
|
338
|
+
}
|
|
339
|
+
const next = chooseNextGoal(plan, options.retryFailed === true);
|
|
340
|
+
if (next) return { goalId: next.id, targetKind: "story" };
|
|
341
|
+
const completion = getUltragoalRunCompletionState(plan, { retryFailed: options.retryFailed });
|
|
342
|
+
if (completion.needsFinalAggregateReceipt) {
|
|
343
|
+
const required = requiredUltragoalGoals(plan);
|
|
344
|
+
const finalGoal = required.at(-1);
|
|
345
|
+
if (finalGoal) return { goalId: finalGoal.id, targetKind: "final_aggregate_receipt" };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Atomic consuming writer. Locks the ledger path, rereads + counts nudge rows for the
|
|
352
|
+
* target story, and appends exactly one `nudge` row inside the same critical section
|
|
353
|
+
* only while budget remains. Reuses the lockless `appendLedger` inside the lock (it
|
|
354
|
+
* does not acquire a conflicting lock), so concurrent guarded attempts cannot both
|
|
355
|
+
* observe `count = budget - 1` and overshoot the budget.
|
|
356
|
+
*/
|
|
357
|
+
export async function recordUltragoalNudgeIfBudgetRemaining(input: {
|
|
358
|
+
cwd: string;
|
|
359
|
+
sessionId?: string | null;
|
|
360
|
+
target: UltragoalNudgeTarget;
|
|
361
|
+
surface: UltragoalNudgeSurface;
|
|
362
|
+
budget: number;
|
|
363
|
+
reason: string;
|
|
364
|
+
currentGoalObjective?: string;
|
|
365
|
+
}): Promise<UltragoalNudgeOutcome> {
|
|
366
|
+
const { cwd, sessionId, target, surface, budget, reason } = input;
|
|
367
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
368
|
+
return {
|
|
369
|
+
nudged: false,
|
|
370
|
+
exhausted: true,
|
|
371
|
+
count: 0,
|
|
372
|
+
budget: Math.max(0, budget | 0),
|
|
373
|
+
goalId: target.goalId,
|
|
374
|
+
targetKind: target.targetKind,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const resolvedSessionId =
|
|
378
|
+
sessionId?.trim() || resolveSkcSessionForWrite(cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId;
|
|
379
|
+
const paths = getUltragoalPaths(cwd, resolvedSessionId);
|
|
380
|
+
return withWorkflowStateLock(
|
|
381
|
+
paths.ledgerPath,
|
|
382
|
+
async () => {
|
|
383
|
+
const ledger = await readUltragoalLedger(cwd, resolvedSessionId);
|
|
384
|
+
const count = countUltragoalNudges(ledger, target.goalId);
|
|
385
|
+
if (count >= budget) {
|
|
386
|
+
return {
|
|
387
|
+
nudged: false,
|
|
388
|
+
exhausted: true,
|
|
389
|
+
count,
|
|
390
|
+
budget,
|
|
391
|
+
goalId: target.goalId,
|
|
392
|
+
targetKind: target.targetKind,
|
|
393
|
+
} as const;
|
|
394
|
+
}
|
|
395
|
+
const attempt = count + 1;
|
|
396
|
+
const entry = (await appendLedger(
|
|
397
|
+
cwd,
|
|
398
|
+
{
|
|
399
|
+
event: "nudge",
|
|
400
|
+
goalId: target.goalId,
|
|
401
|
+
targetKind: target.targetKind,
|
|
402
|
+
surface,
|
|
403
|
+
attempt,
|
|
404
|
+
budget,
|
|
405
|
+
reason,
|
|
406
|
+
...(input.currentGoalObjective ? { currentGoalObjective: input.currentGoalObjective } : {}),
|
|
407
|
+
},
|
|
408
|
+
resolvedSessionId,
|
|
409
|
+
)) as UltragoalNudgeLedgerEvent;
|
|
410
|
+
return {
|
|
411
|
+
nudged: true,
|
|
412
|
+
attempt,
|
|
413
|
+
budget,
|
|
414
|
+
goalId: target.goalId,
|
|
415
|
+
targetKind: target.targetKind,
|
|
416
|
+
event: entry,
|
|
417
|
+
} as const;
|
|
418
|
+
},
|
|
419
|
+
{ cwd },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
222
423
|
async function writePlan(cwd: string, plan: UltragoalPlan, sessionId?: string | null): Promise<void> {
|
|
223
424
|
const resolvedSessionId =
|
|
224
425
|
sessionId?.trim() || resolveSkcSessionForWrite(cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId;
|
|
@@ -524,6 +725,20 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
|
|
|
524
725
|
else if (counts.active > 0) status = "active";
|
|
525
726
|
else if (counts.failed > 0) status = "failed";
|
|
526
727
|
else if (counts.blocked > 0 || counts.review_blocked > 0) status = "blocked";
|
|
728
|
+
const nudgeTarget = selectUltragoalNudgeTarget(plan, { currentGoalObjective: currentGoal?.objective });
|
|
729
|
+
let nudgeFields: Partial<UltragoalStatusSummary> = {};
|
|
730
|
+
if (nudgeTarget) {
|
|
731
|
+
const { budget } = await resolveUltragoalNudgeBudget(cwd);
|
|
732
|
+
const ledger = await readUltragoalLedger(cwd, resolvedSessionId);
|
|
733
|
+
const nudgeCount = countUltragoalNudges(ledger, nudgeTarget.goalId);
|
|
734
|
+
nudgeFields = {
|
|
735
|
+
nudgeBudget: budget,
|
|
736
|
+
nudgeCount,
|
|
737
|
+
nudgeRemaining: Math.max(0, budget - nudgeCount),
|
|
738
|
+
nudgeGoalId: nudgeTarget.goalId,
|
|
739
|
+
nudgeTargetKind: nudgeTarget.targetKind,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
527
742
|
return {
|
|
528
743
|
exists: true,
|
|
529
744
|
status,
|
|
@@ -532,6 +747,7 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
|
|
|
532
747
|
currentGoal,
|
|
533
748
|
counts,
|
|
534
749
|
goals: plan.goals,
|
|
750
|
+
...nudgeFields,
|
|
535
751
|
};
|
|
536
752
|
}
|
|
537
753
|
export function buildUltragoalHudSummary(
|
|
@@ -831,6 +1047,14 @@ function normalizedEvidenceKind(row: JsonObject): string {
|
|
|
831
1047
|
function evidenceKindMatches(kind: string, words: string[]): boolean {
|
|
832
1048
|
return words.some(word => kind.includes(word));
|
|
833
1049
|
}
|
|
1050
|
+
function formatActualArtifactKinds(artifactIds: string[], kinds: string[]): string {
|
|
1051
|
+
if (artifactIds.length === 0) return "none";
|
|
1052
|
+
return artifactIds.map((id, index) => `${id}=${kinds[index] ?? "<missing-kind>"}`).join(", ");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function formatExpectedKindWords(words: string[]): string {
|
|
1056
|
+
return words.map(word => `"${word}"`).join(", ");
|
|
1057
|
+
}
|
|
834
1058
|
|
|
835
1059
|
type SurfaceFamily = "web" | "cli" | "native" | "api-package" | "algorithm-math" | "unknown";
|
|
836
1060
|
|
|
@@ -899,9 +1123,18 @@ function categorizeComputerChangePath(value: string): UltragoalChangeCategory {
|
|
|
899
1123
|
}
|
|
900
1124
|
|
|
901
1125
|
function isComputerControlSurfaceCategory(category: UltragoalChangeCategory): boolean {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
)
|
|
1126
|
+
// The computer-use red-team suite is conditional, not universal (see the
|
|
1127
|
+
// ultragoal SKILL): require it only when the change actually touches
|
|
1128
|
+
// computer-control source — the computer tool (`tool`), its settings/registry
|
|
1129
|
+
// wiring (`settings-registry`), or computer Rust (`code`). A bare regeneration
|
|
1130
|
+
// of the SHARED native binding (`generated-binding`: packages/natives/native/
|
|
1131
|
+
// index.{d.ts,js}) is NOT by itself a computer-use change: that file is
|
|
1132
|
+
// generated from Rust, so any real computer-use behavior change must also
|
|
1133
|
+
// touch one of the categories above and will still trigger the suite. Treating
|
|
1134
|
+
// the regenerated aggregate binding as a computer surface forced the suite on
|
|
1135
|
+
// unrelated features (e.g. notifications), which the SKILL explicitly warns
|
|
1136
|
+
// against, so it is excluded here.
|
|
1137
|
+
return category === "code" || category === "tool" || category === "settings-registry";
|
|
905
1138
|
}
|
|
906
1139
|
|
|
907
1140
|
function isComputerControlSurfaceChangePath(row: UltragoalChangeSetPath): boolean {
|
|
@@ -967,7 +1200,7 @@ function validateSurfaceArtifactCompatibility(
|
|
|
967
1200
|
const hasVisual = kinds.some(kind => evidenceKindMatches(kind, ["screenshot", "image", "visual"]));
|
|
968
1201
|
if (!hasBrowser || !hasVisual) {
|
|
969
1202
|
throw new Error(
|
|
970
|
-
`qualityGate ${fieldName} for GUI/web surfaces must reference browser automation plus screenshot or image-verdict artifacts`,
|
|
1203
|
+
`qualityGate ${fieldName} for GUI/web surfaces must reference browser automation plus screenshot or image-verdict artifacts; surface "${surface}" expected one artifact kind containing one of ${formatExpectedKindWords(["browser", "playwright", "pandawright", "automation"])} and one containing one of ${formatExpectedKindWords(["screenshot", "image", "visual"])}; actual artifact kinds: ${formatActualArtifactKinds(artifactIds, kinds)}`,
|
|
971
1204
|
);
|
|
972
1205
|
}
|
|
973
1206
|
return;
|
|
@@ -994,7 +1227,7 @@ function validateSurfaceArtifactCompatibility(
|
|
|
994
1227
|
const expected = surfaceFamilies[family];
|
|
995
1228
|
if (!kinds.some(kind => evidenceKindMatches(kind, expected.evidence))) {
|
|
996
1229
|
throw new Error(
|
|
997
|
-
`qualityGate ${fieldName} for ${expected.label} surfaces must reference compatible artifact kinds`,
|
|
1230
|
+
`qualityGate ${fieldName} for ${expected.label} surfaces must reference compatible artifact kinds; surface "${surface}" expected at least one artifact kind containing one of ${formatExpectedKindWords(expected.evidence)}; actual artifact kinds: ${formatActualArtifactKinds(artifactIds, kinds)}`,
|
|
998
1231
|
);
|
|
999
1232
|
}
|
|
1000
1233
|
}
|
|
@@ -1490,6 +1723,20 @@ function isAllowedCliReplayCommand(command: readonly string[]): boolean {
|
|
|
1490
1723
|
if (executable === "skc") return args.length === 1 && ["read", "status"].includes(args[0] ?? "");
|
|
1491
1724
|
return false;
|
|
1492
1725
|
}
|
|
1726
|
+
function summarizeBlockedCliReplayCommand(command: readonly string[]): string {
|
|
1727
|
+
const executable = command[0] ? basenameCommand(command[0]) : "<missing>";
|
|
1728
|
+
const argCount = Math.max(0, command.length - 1);
|
|
1729
|
+
return `${JSON.stringify(executable)} with ${argCount} arg${argCount === 1 ? "" : "s"}`;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function cliReplayAllowlistDescription(): string {
|
|
1733
|
+
return [
|
|
1734
|
+
'`bun --version`, `node --version`, or deterministic `bun/node -e "console.log(...)"`',
|
|
1735
|
+
"`npm|pnpm|yarn --version` or `npm|pnpm|yarn list`",
|
|
1736
|
+
"read-only `git status|rev-parse|merge-base|diff|show|log` with safe args",
|
|
1737
|
+
"`skc read` or `skc status`",
|
|
1738
|
+
].join("; ");
|
|
1739
|
+
}
|
|
1493
1740
|
|
|
1494
1741
|
function resolveCliReplayCommand(command: string[]): string[] {
|
|
1495
1742
|
if (basenameCommand(command[0]!) === "bun") return [process.execPath, ...command.slice(1)];
|
|
@@ -1578,8 +1825,11 @@ function parseCliReplayRecord(
|
|
|
1578
1825
|
if (!command) throw new Error(`qualityGate ${fieldName}.command must be a non-empty string array`);
|
|
1579
1826
|
if (record.replaySafe !== true)
|
|
1580
1827
|
throw new Error(`qualityGate ${fieldName}.replaySafe must be true before CLI replay executes`);
|
|
1581
|
-
if (!isAllowedCliReplayCommand(command))
|
|
1582
|
-
throw new Error(
|
|
1828
|
+
if (!isAllowedCliReplayCommand(command)) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
`qualityGate ${fieldName}.command is not in the conservative CLI replay allowlist; command ${summarizeBlockedCliReplayCommand(command)} is blocked. Allowed replay commands: ${cliReplayAllowlistDescription()}. For other commands, provide audited replayExempt metadata with reasonCode, reason, approvedBy, and fallbackArtifactRefs that point to a structurally valid fallback artifact.`,
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1583
1833
|
if (record.normalization !== undefined && record.normalization !== "default") {
|
|
1584
1834
|
throw new Error(`qualityGate ${fieldName}.normalization must be default when provided`);
|
|
1585
1835
|
}
|
|
@@ -2231,6 +2481,28 @@ function snapshotUpdatedAtMilliseconds(value: unknown): number | null {
|
|
|
2231
2481
|
const parsed = Date.parse(trimmed);
|
|
2232
2482
|
return Number.isFinite(parsed) ? parsed : null;
|
|
2233
2483
|
}
|
|
2484
|
+
|
|
2485
|
+
function singleSessionLeafId(entries: readonly SessionEntry[]): string | undefined {
|
|
2486
|
+
if (entries.length === 0) return undefined;
|
|
2487
|
+
const parentIds = new Set(
|
|
2488
|
+
entries.map(entry => entry.parentId).filter((parentId): parentId is string => typeof parentId === "string"),
|
|
2489
|
+
);
|
|
2490
|
+
const leafIds = entries.map(entry => entry.id).filter(id => !parentIds.has(id));
|
|
2491
|
+
return leafIds.length === 1 ? leafIds[0] : undefined;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
async function readCurrentSessionSkcGoalSnapshot(): Promise<unknown | undefined> {
|
|
2495
|
+
const sessionFile = process.env[SKC_SESSION_FILE_ENV]?.trim();
|
|
2496
|
+
if (!sessionFile) return undefined;
|
|
2497
|
+
const fileEntries = await loadEntriesFromFile(sessionFile);
|
|
2498
|
+
const entries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session");
|
|
2499
|
+
const leafId = singleSessionLeafId(entries);
|
|
2500
|
+
if (!leafId) return undefined;
|
|
2501
|
+
const context = buildSessionContext(entries, leafId);
|
|
2502
|
+
if (context.mode !== "goal" && context.mode !== "goal_paused") return undefined;
|
|
2503
|
+
const goal = normalizeGoal(context.modeData?.goal);
|
|
2504
|
+
return goal ? { goal } : undefined;
|
|
2505
|
+
}
|
|
2234
2506
|
async function readSkcGoalSnapshot(input: {
|
|
2235
2507
|
cwd: string;
|
|
2236
2508
|
value: string | undefined;
|
|
@@ -2240,13 +2512,15 @@ async function readSkcGoalSnapshot(input: {
|
|
|
2240
2512
|
errorPrefix: string;
|
|
2241
2513
|
allowCompletedLegacyBlocker?: boolean;
|
|
2242
2514
|
}): Promise<unknown> {
|
|
2243
|
-
|
|
2515
|
+
const snapshot = input.value?.trim()
|
|
2516
|
+
? await readStructuredValue(input.cwd, input.value)
|
|
2517
|
+
: await readCurrentSessionSkcGoalSnapshot();
|
|
2518
|
+
if (snapshot === undefined) {
|
|
2244
2519
|
if (!input.required) return undefined;
|
|
2245
2520
|
throw new Error(
|
|
2246
|
-
`${input.errorPrefix} require
|
|
2521
|
+
`${input.errorPrefix} require an active SKC goal-mode snapshot from the current session or --skc-goal-json; this is the SKC goal-mode receipt, not the .skc/ultragoal/goals.json goal record`,
|
|
2247
2522
|
);
|
|
2248
2523
|
}
|
|
2249
|
-
const snapshot = await readStructuredValue(input.cwd, input.value);
|
|
2250
2524
|
const snapshotObject = qualityGateObject(snapshot);
|
|
2251
2525
|
const detailsObject = qualityGateObject(snapshotObject?.details);
|
|
2252
2526
|
const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
|
|
@@ -3331,18 +3605,19 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
3331
3605
|
" --status=<value> pending|active|complete|failed|blocked|review_blocked|superseded",
|
|
3332
3606
|
" --evidence=<value> Completion or checkpoint evidence text",
|
|
3333
3607
|
" --quality-gate-json=<value> JSON string or path for complete checkpoints",
|
|
3334
|
-
|
|
3608
|
+
" --skc-goal-json=<value> Optional JSON/path override for current goal snapshot; omitted complete checkpoints read current session goal state",
|
|
3335
3609
|
" --json Output a machine-readable receipt",
|
|
3336
3610
|
"",
|
|
3337
3611
|
"COMPLETE CHECKPOINT RECEIPTS",
|
|
3338
3612
|
" --quality-gate-json must be an object with architectReview, executorQa, and iteration.",
|
|
3339
3613
|
" executorQa.contractCoverage[] rows require an obligation field; description is not a substitute.",
|
|
3340
|
-
|
|
3614
|
+
" Complete checkpoints use the current session's active SKC goal-mode snapshot when --skc-goal-json is omitted.",
|
|
3615
|
+
" Explicit --skc-goal-json values must contain an active SKC goal-mode snapshot, not the .skc/ultragoal/goals.json goal record.",
|
|
3341
3616
|
" goal.updatedAt may be epoch milliseconds or an ISO timestamp and must be fresh.",
|
|
3342
3617
|
"",
|
|
3343
3618
|
"EXAMPLES",
|
|
3344
3619
|
' $ skc ultragoal checkpoint --goal-id G001 --status blocked --evidence "waiting on review"',
|
|
3345
|
-
' $ skc ultragoal checkpoint --goal-id G001 --status complete --evidence "tests passed" --
|
|
3620
|
+
' $ skc ultragoal checkpoint --goal-id G001 --status complete --evidence "tests passed" --quality-gate-json ./quality-gate.json --json',
|
|
3346
3621
|
"",
|
|
3347
3622
|
].join("\n");
|
|
3348
3623
|
}
|
|
@@ -3787,6 +4062,11 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
|
|
|
3787
4062
|
goals_path: summary.paths.goalsPath,
|
|
3788
4063
|
};
|
|
3789
4064
|
if (summary.skcObjective) payload.skc_objective = summary.skcObjective;
|
|
4065
|
+
if (summary.nudgeBudget !== undefined) payload.nudge_budget = summary.nudgeBudget;
|
|
4066
|
+
if (summary.nudgeCount !== undefined) payload.nudge_count = summary.nudgeCount;
|
|
4067
|
+
if (summary.nudgeRemaining !== undefined) payload.nudge_remaining = summary.nudgeRemaining;
|
|
4068
|
+
if (summary.nudgeGoalId !== undefined) payload.nudge_goal_id = summary.nudgeGoalId;
|
|
4069
|
+
if (summary.nudgeTargetKind !== undefined) payload.nudge_target_kind = summary.nudgeTargetKind;
|
|
3790
4070
|
const ledgerText = await Bun.file(summary.paths.ledgerPath)
|
|
3791
4071
|
.text()
|
|
3792
4072
|
.catch(() => "");
|
|
@@ -6,6 +6,7 @@ import { getOAuthProviders } from "@sayknow-cli/ai/utils/oauth";
|
|
|
6
6
|
import { Spacer, Text } from "@sayknow-cli/tui";
|
|
7
7
|
import { setProjectDir } from "@sayknow-cli/utils";
|
|
8
8
|
import { jobElapsedMs } from "../async";
|
|
9
|
+
import { materializeActiveModelProfileAssignment } from "../config/model-profile-activation";
|
|
9
10
|
import {
|
|
10
11
|
SKC_MODEL_ASSIGNMENT_TARGET_IDS,
|
|
11
12
|
SKC_MODEL_ASSIGNMENT_TARGETS,
|
|
@@ -299,6 +300,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
299
300
|
selector: selection.selector,
|
|
300
301
|
thinkingLevel: selection.thinkingLevel,
|
|
301
302
|
});
|
|
303
|
+
materializeActiveModelProfileAssignment({
|
|
304
|
+
session: runtime.session,
|
|
305
|
+
settings: runtime.settings,
|
|
306
|
+
role: parsedArgs.targetId,
|
|
307
|
+
selector: persistedSelector,
|
|
308
|
+
});
|
|
302
309
|
if (selection.thinkingLevel) {
|
|
303
310
|
runtime.session.setThinkingLevel(selection.thinkingLevel);
|
|
304
311
|
}
|
|
@@ -317,10 +324,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
317
324
|
selection.thinkingLevel ??
|
|
318
325
|
extractExplicitThinkingSelector(overrides[parsedArgs.targetId], runtime.settings);
|
|
319
326
|
const roleSelector = formatModelSelectorValue(selection.selector, thinkingLevel);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
327
|
+
const materializedProfile = materializeActiveModelProfileAssignment({
|
|
328
|
+
session: runtime.session,
|
|
329
|
+
settings: runtime.settings,
|
|
330
|
+
role: parsedArgs.targetId,
|
|
331
|
+
selector: roleSelector,
|
|
323
332
|
});
|
|
333
|
+
if (!materializedProfile) {
|
|
334
|
+
const target = SKC_MODEL_ASSIGNMENT_TARGETS[parsedArgs.targetId];
|
|
335
|
+
if (target.settingsPath === "modelRoles") {
|
|
336
|
+
runtime.settings.setModelRole(parsedArgs.targetId, roleSelector);
|
|
337
|
+
} else {
|
|
338
|
+
runtime.settings.set("task.agentModelOverrides", {
|
|
339
|
+
...overrides,
|
|
340
|
+
[parsedArgs.targetId]: roleSelector,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
324
344
|
runtime.settings.getStorage()?.recordModelUsage(`${selection.model.provider}/${selection.model.id}`);
|
|
325
345
|
await runtime.output(`${parsedArgs.targetId} agent model set to ${roleSelector}.`);
|
|
326
346
|
}
|
|
@@ -56,6 +56,12 @@ export interface FastStatusSessionLike {
|
|
|
56
56
|
readonly model?: Model;
|
|
57
57
|
/** Fast predicate against the main session tier (current model + `modelRoles`). */
|
|
58
58
|
isFastForProvider(provider?: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Current-model EFFECTIVE fast state (intent minus any provider auto-disable).
|
|
61
|
+
* Used for the current-model row so it matches what the next request does.
|
|
62
|
+
* Optional so lightweight fakes can omit it; falls back to `isFastForProvider`.
|
|
63
|
+
*/
|
|
64
|
+
isFastModeActive?(): boolean;
|
|
59
65
|
/** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
|
|
60
66
|
isFastForSubagentProvider(provider?: string): boolean;
|
|
61
67
|
resolveRoleModelWithThinking(role: string): { model?: Model };
|
|
@@ -95,9 +101,13 @@ export interface BuildFastStatusReportArgs {
|
|
|
95
101
|
*/
|
|
96
102
|
export function buildFastStatusReport(args: BuildFastStatusReportArgs): string {
|
|
97
103
|
const { session, roleTargets, iconFast, formatInactive } = args;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
// Current-model row uses the EFFECTIVE predicate (intent minus any provider
|
|
105
|
+
// auto-disable) so it matches the next request; `modelRoles` rows below stay
|
|
106
|
+
// on pure intent. Fall back to intent when a fake omits `isFastModeActive`.
|
|
107
|
+
const currentFast = session.isFastModeActive
|
|
108
|
+
? session.isFastModeActive()
|
|
109
|
+
: session.isFastForProvider(session.model?.provider);
|
|
110
|
+
const rows: FastStatusRow[] = [{ label: "현재 모델", model: session.model, fast: currentFast }];
|
|
101
111
|
for (const target of roleTargets) {
|
|
102
112
|
const resolved = session.resolveRoleModelWithThinking(target.id);
|
|
103
113
|
if (resolved.model) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseCommandArgs } from "../../utils/command-args";
|
|
1
2
|
import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
|
|
2
3
|
|
|
3
4
|
export interface ParsedSubcommand {
|
|
@@ -65,7 +66,7 @@ export function errorMessage(error: unknown): string {
|
|
|
65
66
|
* "name required" diagnostics with their own messaging.
|
|
66
67
|
*/
|
|
67
68
|
export function parseNamedScopeArgs(rest: string, invalidScopeMessage: string): NamedScopeArgs {
|
|
68
|
-
const tokens = rest
|
|
69
|
+
const tokens = parseCommandArgs(rest);
|
|
69
70
|
let name: string | undefined;
|
|
70
71
|
let scope: ConfigScope = "project";
|
|
71
72
|
let i = 0;
|
package/src/system-prompt.ts
CHANGED
|
@@ -336,6 +336,8 @@ export interface BuildSystemPromptOptions {
|
|
|
336
336
|
toolNames?: string[];
|
|
337
337
|
/** Text to append to system prompt. */
|
|
338
338
|
appendSystemPrompt?: string;
|
|
339
|
+
/** Rendered SKC plugin system-appendix blocks (lower-authority, appended last). */
|
|
340
|
+
pluginAppendices?: string;
|
|
339
341
|
/** Repeat full tool descriptions in system prompt. Default: false */
|
|
340
342
|
repeatToolDescriptions?: boolean;
|
|
341
343
|
/** Skills settings for discovery. */
|
|
@@ -380,6 +382,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
380
382
|
customPrompt,
|
|
381
383
|
tools,
|
|
382
384
|
appendSystemPrompt,
|
|
385
|
+
pluginAppendices,
|
|
383
386
|
repeatToolDescriptions = false,
|
|
384
387
|
skillsSettings,
|
|
385
388
|
toolNames: providedToolNames,
|
|
@@ -579,5 +582,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
579
582
|
systemPrompt.push(projectPrompt);
|
|
580
583
|
}
|
|
581
584
|
|
|
585
|
+
// Plugin system appendices are appended last as a lower-authority block; they
|
|
586
|
+
// can never override base/project/developer instructions above them.
|
|
587
|
+
if (pluginAppendices?.trim()) {
|
|
588
|
+
systemPrompt.push(pluginAppendices.trim());
|
|
589
|
+
}
|
|
590
|
+
|
|
582
591
|
return { systemPrompt };
|
|
583
592
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as fs from "node:fs/promises";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
11
11
|
import { recordHandoff, resolveTelemetry } from "@sayknow-cli/agent-core";
|
|
12
|
+
import type { ServiceTier } from "@sayknow-cli/ai";
|
|
12
13
|
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@sayknow-cli/ai/utils/schema";
|
|
13
14
|
import { logger, prompt, untilAborted } from "@sayknow-cli/utils";
|
|
14
15
|
import { AsyncJobManager } from "../async";
|
|
@@ -19,7 +20,7 @@ import { Settings } from "../config/settings";
|
|
|
19
20
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
20
21
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
21
22
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
22
|
-
import { buildAgentSubskillInjection } from "../extensibility/skc-plugins";
|
|
23
|
+
import { buildAgentSubskillInjection, renderAgentPromptAdditions } from "../extensibility/skc-plugins";
|
|
23
24
|
import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
|
|
24
25
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
25
26
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
@@ -146,6 +147,13 @@ export interface ExecutorOptions {
|
|
|
146
147
|
authStorage?: AuthStorage;
|
|
147
148
|
modelRegistry?: ModelRegistry;
|
|
148
149
|
settings?: Settings;
|
|
150
|
+
/**
|
|
151
|
+
* Live service-tier intent of the parent session (`AgentSession.serviceTier`),
|
|
152
|
+
* used as the inherited tier when `task.serviceTier === "inherit"`. Passing the
|
|
153
|
+
* live value (not the stale settings snapshot) lets a runtime `/fast on` reach
|
|
154
|
+
* subagents, and a main-model fast-mode auto-disable does not clobber it.
|
|
155
|
+
*/
|
|
156
|
+
inheritedServiceTier?: ServiceTier;
|
|
149
157
|
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
150
158
|
localProtocolOptions?: LocalProtocolOptions;
|
|
151
159
|
/**
|
|
@@ -482,15 +490,19 @@ function getUsageTokens(usage: unknown): number {
|
|
|
482
490
|
return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
|
|
483
491
|
}
|
|
484
492
|
|
|
485
|
-
export function createSubagentSettings(baseSettings: Settings): Settings {
|
|
493
|
+
export function createSubagentSettings(baseSettings: Settings, inheritedServiceTier?: ServiceTier): Settings {
|
|
486
494
|
const snapshot: Partial<Record<SettingPath, unknown>> = {};
|
|
487
495
|
for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
488
496
|
snapshot[key] = baseSettings.get(key);
|
|
489
497
|
}
|
|
490
|
-
// Subagent-scoped service-tier override: "inherit"
|
|
491
|
-
//
|
|
498
|
+
// Subagent-scoped service-tier override: "inherit" uses the parent session's
|
|
499
|
+
// LIVE intent (so a runtime `/fast on` reaches subagents and a main-model
|
|
500
|
+
// fast-mode auto-disable never clobbers it); any explicit value applies only
|
|
501
|
+
// to subagent sessions and wins over inherited intent.
|
|
492
502
|
const taskServiceTier = baseSettings.get("task.serviceTier");
|
|
493
|
-
if (taskServiceTier
|
|
503
|
+
if (taskServiceTier === "inherit") {
|
|
504
|
+
snapshot.serviceTier = inheritedServiceTier ?? "none";
|
|
505
|
+
} else {
|
|
494
506
|
snapshot.serviceTier = taskServiceTier;
|
|
495
507
|
}
|
|
496
508
|
return Settings.isolated({
|
|
@@ -571,7 +583,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
571
583
|
}
|
|
572
584
|
|
|
573
585
|
const settings = options.settings ?? Settings.isolated();
|
|
574
|
-
const subagentSettings = createSubagentSettings(settings);
|
|
586
|
+
const subagentSettings = createSubagentSettings(settings, options.inheritedServiceTier);
|
|
575
587
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
576
588
|
const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
|
|
577
589
|
const parentDepth = options.taskDepth ?? 0;
|
|
@@ -1229,6 +1241,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1229
1241
|
agentName: agent.name,
|
|
1230
1242
|
});
|
|
1231
1243
|
|
|
1244
|
+
let agentPromptAdditions = { appendix: "", advertisement: "" };
|
|
1245
|
+
try {
|
|
1246
|
+
agentPromptAdditions = await renderAgentPromptAdditions({ cwd, agentName: agent.name });
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
logger.warn("Failed to render SKC plugin agent prompt additions", { error });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1232
1251
|
const { session } = await awaitAbortable(
|
|
1233
1252
|
createAgentSession({
|
|
1234
1253
|
cwd: worktree ?? cwd,
|
|
@@ -1259,7 +1278,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1259
1278
|
ircSelfId: ircEnabled ? id : "",
|
|
1260
1279
|
forkContext: forkContextNotice,
|
|
1261
1280
|
});
|
|
1262
|
-
|
|
1281
|
+
// Order: base agent prompt -> agent appendix -> Tier-1 advertisement -> Tier-2 body.
|
|
1282
|
+
const appendixPart = agentPromptAdditions.appendix ? `\n\n${agentPromptAdditions.appendix}` : "";
|
|
1283
|
+
const advertPart = agentPromptAdditions.advertisement
|
|
1284
|
+
? `\n\n${agentPromptAdditions.advertisement}`
|
|
1285
|
+
: "";
|
|
1286
|
+
const promptWithSubskills = `${subagentPrompt}${appendixPart}${advertPart}${agentSubskillBlock}`;
|
|
1263
1287
|
return defaultPrompt.length === 0
|
|
1264
1288
|
? [promptWithSubskills]
|
|
1265
1289
|
: [...defaultPrompt.slice(0, -1), promptWithSubskills, defaultPrompt[defaultPrompt.length - 1]];
|
package/src/task/index.ts
CHANGED
|
@@ -1311,6 +1311,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1311
1311
|
authStorage: this.session.authStorage,
|
|
1312
1312
|
modelRegistry: this.session.modelRegistry,
|
|
1313
1313
|
settings: this.session.settings,
|
|
1314
|
+
inheritedServiceTier: this.session.serviceTier,
|
|
1314
1315
|
contextFiles,
|
|
1315
1316
|
skills: availableSkills,
|
|
1316
1317
|
autoloadSkills: resolvedAutoloadSkills,
|
|
@@ -1372,6 +1373,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1372
1373
|
authStorage: this.session.authStorage,
|
|
1373
1374
|
modelRegistry: this.session.modelRegistry,
|
|
1374
1375
|
settings: this.session.settings,
|
|
1376
|
+
inheritedServiceTier: this.session.serviceTier,
|
|
1375
1377
|
contextFiles,
|
|
1376
1378
|
skills: availableSkills,
|
|
1377
1379
|
autoloadSkills: resolvedAutoloadSkills,
|