@gajae-code/coding-agent 0.16.0 → 0.16.3
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 +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -484,6 +484,18 @@ interface CoordinatorEventInput {
|
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
const UNOBSERVED_COMPENSATION_CODE = "broker_compensation_unobserved";
|
|
487
|
+
const OBSERVED_BROKER_FAILURE_CODES = new Set([
|
|
488
|
+
"worktree_preparation_timeout",
|
|
489
|
+
"dependency_preparation_timeout",
|
|
490
|
+
"readiness_timeout",
|
|
491
|
+
"spawn_failed",
|
|
492
|
+
"worktree_in_use",
|
|
493
|
+
"startup_admission_timeout",
|
|
494
|
+
"startup_admission_refused",
|
|
495
|
+
"incarnation_unavailable",
|
|
496
|
+
"invalid_input",
|
|
497
|
+
"broker_request_unavailable",
|
|
498
|
+
]);
|
|
487
499
|
|
|
488
500
|
const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
|
|
489
501
|
const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
|
|
@@ -1216,6 +1228,14 @@ const PUBLIC_ERROR_MESSAGES: Record<string, string> = {
|
|
|
1216
1228
|
broker_discovery_unavailable: "SDK broker discovery is unavailable.",
|
|
1217
1229
|
broker_request_unavailable: "SDK broker request is unavailable.",
|
|
1218
1230
|
broker_transport_unavailable: "SDK broker transport is unavailable.",
|
|
1231
|
+
startup_admission_timeout: "SDK host startup was not admitted before the queue wait cutoff.",
|
|
1232
|
+
startup_admission_refused: "SDK host startup was refused because the broker no longer owns the session root.",
|
|
1233
|
+
worktree_in_use: "The requested worktree is already held by another live session.",
|
|
1234
|
+
worktree_preparation_timeout: "Worktree preparation exceeded its deadline before the session host was spawned.",
|
|
1235
|
+
dependency_preparation_timeout: "Dependency preparation exceeded its deadline before the session host was spawned.",
|
|
1236
|
+
spawn_failed: "Session host could not be spawned.",
|
|
1237
|
+
readiness_timeout: "Session did not become ready before the SDK readiness deadline.",
|
|
1238
|
+
incarnation_unavailable: "OS process incarnation authority is unavailable.",
|
|
1219
1239
|
session_closing: "Coordinator session is closing.",
|
|
1220
1240
|
session_unavailable: "Coordinator session is unavailable.",
|
|
1221
1241
|
session_not_activatable: "Coordinator session cannot be activated.",
|
|
@@ -5320,7 +5340,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5320
5340
|
} catch (error) {
|
|
5321
5341
|
const sessionId = remoteSession.value;
|
|
5322
5342
|
if (!sessionId) {
|
|
5323
|
-
if (error instanceof SdkClientError && error.code
|
|
5343
|
+
if (error instanceof SdkClientError && OBSERVED_BROKER_FAILURE_CODES.has(error.code)) throw error;
|
|
5324
5344
|
throw new SdkClientError(
|
|
5325
5345
|
UNOBSERVED_COMPENSATION_CODE,
|
|
5326
5346
|
"Coordinator creation outcome could not be identified for cleanup.",
|
|
@@ -378,7 +378,7 @@ Auto-answer has a clarity cap: unless the architect confidence is `high` and unc
|
|
|
378
378
|
|
|
379
379
|
When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections). Then confirm with exactly one `ask` that nothing is lost or misrepresented: the `ask` question body MUST render the full structured interpretation — every non-empty canonical section, verbatim — before the confirmation prompt. The user is approving that specific interpretation, so it must be visible inside the question body; never ask "does this capture it?" / "이 해석이 맞아?" without first displaying the interpretation itself. A confirmation `ask` whose body omits the interpretation it is asking about is a hard error: re-issue it with the interpretation shown. Apply `language.instruction` when present.
|
|
380
380
|
|
|
381
|
-
Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
|
|
381
|
+
Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text through the Ask UI's automatic Other/custom-input control. Never add the automatic custom-input label or a pseudo-option that imitates it, such as **Other (type your own)**, **Other (specify)**, **type your own**, **custom input**, **직접 입력**, or **직접 입력으로 추가**; a plain domain value such as **Other** is valid when it is a genuine answer, not a request for free text, and none of these custom-input labels may receive `recommended`. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` whose question body asks for the text and whose options are only real non-custom choices such as cancel/keep-current (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
|
|
382
382
|
|
|
383
383
|
Skip Refine for short answers with no attached reasoning (e.g. "Yes" / "No" / a single proper noun), for pre-built option picks where the structure is already explicit, for auto-confirmed code/brownfield facts, and for architect auto-answers (already structured by Step 2b′). A refined answer counts as direct user judgment: record the round in `refined_rounds` and reset `auto_answer_streak` to 0. Feed the confirmed structured interpretation — not the raw free text — into Step 2c scoring and established-facts maintenance.
|
|
384
384
|
|
|
@@ -69,6 +69,21 @@ interface BeforeAgentStartCombinedResult {
|
|
|
69
69
|
systemPrompt?: string[];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const BEFORE_AGENT_START_RESULT_KEYS = new Set(["message", "systemPrompt"]);
|
|
73
|
+
const MAX_UNSUPPORTED_RESULT_FIELDS = 8;
|
|
74
|
+
const MAX_RESULT_FIELD_NAME_LENGTH = 80;
|
|
75
|
+
|
|
76
|
+
function unsupportedBeforeAgentStartResultFields(result: unknown): string[] {
|
|
77
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) return [];
|
|
78
|
+
const fields = Object.keys(result).filter(key => !BEFORE_AGENT_START_RESULT_KEYS.has(key));
|
|
79
|
+
const bounded = fields
|
|
80
|
+
.slice(0, MAX_UNSUPPORTED_RESULT_FIELDS)
|
|
81
|
+
.map(key => key.replace(/[^\x20-\x7e]/gu, "?").slice(0, MAX_RESULT_FIELD_NAME_LENGTH));
|
|
82
|
+
if (fields.length > MAX_UNSUPPORTED_RESULT_FIELDS)
|
|
83
|
+
bounded.push(`…and ${fields.length - MAX_UNSUPPORTED_RESULT_FIELDS} more`);
|
|
84
|
+
return bounded;
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
export type ExtensionErrorListener = (error: ExtensionError) => void;
|
|
73
88
|
|
|
74
89
|
/** Bounded timeout for session_shutdown handlers — generous but never infinite. */
|
|
@@ -247,6 +262,7 @@ export class ExtensionRunner {
|
|
|
247
262
|
#getArtifactRangeFn: ExtensionContextActions["getArtifactRange"] = undefined;
|
|
248
263
|
|
|
249
264
|
#getJobsFn: ExtensionContextActions["getJobs"] = undefined;
|
|
265
|
+
#onJobFoldFn: ExtensionContextActions["onJobFold"] = undefined;
|
|
250
266
|
#sdkControlFn: ExtensionContextActions["sdkControl"] = undefined;
|
|
251
267
|
#setSdkPermissionProviderFn: ExtensionContextActions["setSdkPermissionProvider"] = undefined;
|
|
252
268
|
#setSdkClientBridgeFn: ExtensionContextActions["setSdkClientBridge"] = undefined;
|
|
@@ -378,6 +394,7 @@ export class ExtensionRunner {
|
|
|
378
394
|
this.#getArtifactRangeFn = contextActions.getArtifactRange;
|
|
379
395
|
|
|
380
396
|
this.#getJobsFn = contextActions.getJobs;
|
|
397
|
+
this.#onJobFoldFn = contextActions.onJobFold;
|
|
381
398
|
this.#sdkControlFn = contextActions.sdkControl;
|
|
382
399
|
this.#setSdkPermissionProviderFn = contextActions.setSdkPermissionProvider;
|
|
383
400
|
this.#setSdkClientBridgeFn = contextActions.setSdkClientBridge;
|
|
@@ -662,6 +679,7 @@ export class ExtensionRunner {
|
|
|
662
679
|
getArtifactRange: (id, offset, length) => this.#getArtifactRangeFn?.(id, offset, length),
|
|
663
680
|
|
|
664
681
|
getJobs: () => this.#getJobsFn?.(),
|
|
682
|
+
onJobFold: listener => this.#onJobFoldFn?.(listener) ?? (() => {}),
|
|
665
683
|
sdkControl: (operation, input) => this.#sdkControlFn?.(operation, input),
|
|
666
684
|
setSdkPermissionProvider: provider => this.#setSdkPermissionProviderFn?.(provider),
|
|
667
685
|
setSdkClientBridge: bridge => this.#setSdkClientBridgeFn?.(bridge),
|
|
@@ -679,6 +697,7 @@ export class ExtensionRunner {
|
|
|
679
697
|
...(this.#getExtensionsFn ? ["getExtensions"] : []),
|
|
680
698
|
...(this.#getArtifactRangeFn ? ["getArtifactRange"] : []),
|
|
681
699
|
...(this.#getJobsFn ? ["getJobs"] : []),
|
|
700
|
+
...(this.#onJobFoldFn ? ["onJobFold"] : []),
|
|
682
701
|
...(this.#sdkControlFn ? ["sdkControl"] : []),
|
|
683
702
|
...(this.#invokeSkillFn ? ["invokeSkill"] : []),
|
|
684
703
|
...(this.#setPlanModeFn ? ["setPlanMode"] : []),
|
|
@@ -1108,6 +1127,14 @@ export class ExtensionRunner {
|
|
|
1108
1127
|
const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
|
|
1109
1128
|
|
|
1110
1129
|
if (handlerResult) {
|
|
1130
|
+
const unsupportedFields = unsupportedBeforeAgentStartResultFields(handlerResult);
|
|
1131
|
+
if (unsupportedFields.length > 0) {
|
|
1132
|
+
this.emitError({
|
|
1133
|
+
extensionPath: ext.path,
|
|
1134
|
+
event: "before_agent_start",
|
|
1135
|
+
error: `Unsupported before_agent_start result field(s): ${unsupportedFields.join(", ")}. Supported fields: message, systemPrompt.`,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1111
1138
|
const result = handlerResult as BeforeAgentStartEventResult;
|
|
1112
1139
|
if (result.message) {
|
|
1113
1140
|
messages.push(result.message);
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@gajae-code/ai/utils/oauth/types";
|
|
37
37
|
import type * as piCodingAgent from "@gajae-code/coding-agent";
|
|
38
38
|
import type { AutocompleteItem, Component, EditorTheme, KeyId, TUI } from "@gajae-code/tui";
|
|
39
|
+
import type { JobFoldEvent } from "../../async/job-manager";
|
|
39
40
|
import type { KeybindingsManager } from "../../config/keybindings";
|
|
40
41
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
41
42
|
import type { ModelSelectorValue } from "../../config/model-selector-value";
|
|
@@ -511,7 +512,7 @@ export interface ExtensionContext {
|
|
|
511
512
|
/** Run a control-surface mutation inside the session admission boundary. */
|
|
512
513
|
withSdkControlMutation?<T>(body: () => Promise<T>): Promise<T>;
|
|
513
514
|
cycleThinkingLevel(): ThinkingLevel | undefined;
|
|
514
|
-
setQueueMode(kind: "steering" | "follow_up" | "
|
|
515
|
+
setQueueMode(kind: "steering" | "follow_up" | "tool_interrupt", mode: unknown): boolean;
|
|
515
516
|
getSkillState(): unknown;
|
|
516
517
|
getConfigItems(): unknown;
|
|
517
518
|
getBranchCandidates(): unknown;
|
|
@@ -527,6 +528,8 @@ export interface ExtensionContext {
|
|
|
527
528
|
| Promise<{ bytes: Uint8Array; totalBytes: number } | undefined>;
|
|
528
529
|
|
|
529
530
|
getJobs(): unknown;
|
|
531
|
+
/** Subscribe to foreground-wait folds (`bash_folded`). Returns an unsubscribe function. */
|
|
532
|
+
onJobFold?(listener: (event: JobFoldEvent) => void): () => void;
|
|
530
533
|
/** Typed skill and mode controls exposed to the SDK host. */
|
|
531
534
|
invokeSkill?(
|
|
532
535
|
name: string,
|
|
@@ -1103,7 +1106,7 @@ export type { ToolResultEventResult } from "../shared-events";
|
|
|
1103
1106
|
|
|
1104
1107
|
export interface BeforeAgentStartEventResult {
|
|
1105
1108
|
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details" | "attribution">;
|
|
1106
|
-
/** Replace the system prompt for this turn.
|
|
1109
|
+
/** Replace the system prompt for this turn. To append, spread event.systemPrompt into the returned array. */
|
|
1107
1110
|
systemPrompt?: string[];
|
|
1108
1111
|
}
|
|
1109
1112
|
|
|
@@ -1672,7 +1675,7 @@ export interface ExtensionContextActions {
|
|
|
1672
1675
|
getActiveModelProfile?: () => string | undefined;
|
|
1673
1676
|
withSdkControlMutation?: <T>(body: () => Promise<T>) => Promise<T>;
|
|
1674
1677
|
cycleThinkingLevel?: () => ThinkingLevel | undefined;
|
|
1675
|
-
setQueueMode?: (kind: "steering" | "follow_up" | "
|
|
1678
|
+
setQueueMode?: (kind: "steering" | "follow_up" | "tool_interrupt", mode: unknown) => boolean;
|
|
1676
1679
|
getSkillState?: () => unknown;
|
|
1677
1680
|
getConfigItems?: () => unknown;
|
|
1678
1681
|
getBranchCandidates?: () => unknown;
|
|
@@ -1687,6 +1690,8 @@ export interface ExtensionContextActions {
|
|
|
1687
1690
|
| undefined
|
|
1688
1691
|
| Promise<{ bytes: Uint8Array; totalBytes: number } | undefined>;
|
|
1689
1692
|
getJobs?: () => unknown;
|
|
1693
|
+
/** Subscribe to foreground-wait folds so the SDK host can publish `bash_folded`. Returns an unsubscribe. */
|
|
1694
|
+
onJobFold?: (listener: (event: JobFoldEvent) => void) => () => void;
|
|
1690
1695
|
setSdkPermissionProvider?: (
|
|
1691
1696
|
provider:
|
|
1692
1697
|
| ((
|
|
@@ -54,6 +54,11 @@ export type GjcBundleTransactionResult =
|
|
|
54
54
|
const TAR_MAX_FILES = 8192;
|
|
55
55
|
const TAR_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
56
56
|
const TAR_MAX_TOTAL_BYTES = 128 * 1024 * 1024;
|
|
57
|
+
// Preserve the existing aggregate file budget plus the maximum per-entry
|
|
58
|
+
// header/padding overhead and the two terminating TAR blocks.
|
|
59
|
+
const TAR_MAX_ARCHIVE_BYTES = TAR_MAX_TOTAL_BYTES + TAR_MAX_FILES * 1024 + 1024;
|
|
60
|
+
// A gzip stream can be slightly larger than its incompressible TAR payload.
|
|
61
|
+
const TAR_MAX_COMPRESSED_BYTES = TAR_MAX_ARCHIVE_BYTES + 1024 * 1024;
|
|
57
62
|
|
|
58
63
|
function scopeRoot(scope: GjcPluginScope, cwd: string): string {
|
|
59
64
|
return scope === "user" ? gjcPluginUserRoot() : gjcPluginProjectRoot(cwd);
|
|
@@ -95,6 +100,45 @@ export class GjcPluginSourceUnavailableError extends Error {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
async function readTarballInput(tarPath: string, maxBytes: number): Promise<Buffer> {
|
|
104
|
+
let file: fs.FileHandle;
|
|
105
|
+
try {
|
|
106
|
+
file = await fs.open(tarPath, "r");
|
|
107
|
+
} catch {
|
|
108
|
+
throw new GjcPluginSourceUnavailableError();
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const stat = await file.stat();
|
|
112
|
+
if (!stat.isFile()) throw new GjcPluginSourceUnavailableError();
|
|
113
|
+
if (stat.size > maxBytes) {
|
|
114
|
+
throw new GjcPluginLoadError("security_policy", "GJC plugin tarball exceeds the archive input limit");
|
|
115
|
+
}
|
|
116
|
+
const raw = Buffer.allocUnsafe(stat.size);
|
|
117
|
+
let offset = 0;
|
|
118
|
+
while (offset < raw.byteLength) {
|
|
119
|
+
const { bytesRead } = await file.read(raw, offset, raw.byteLength - offset, offset);
|
|
120
|
+
if (bytesRead === 0) {
|
|
121
|
+
throw new GjcPluginLoadError("security_policy", "GJC plugin tarball changed while being read");
|
|
122
|
+
}
|
|
123
|
+
offset += bytesRead;
|
|
124
|
+
}
|
|
125
|
+
const probe = Buffer.allocUnsafe(1);
|
|
126
|
+
if ((await file.read(probe, 0, 1, offset)).bytesRead !== 0) {
|
|
127
|
+
throw new GjcPluginLoadError("security_policy", "GJC plugin tarball changed while being read");
|
|
128
|
+
}
|
|
129
|
+
return raw;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof GjcPluginLoadError || error instanceof GjcPluginSourceUnavailableError) throw error;
|
|
132
|
+
throw new GjcPluginSourceUnavailableError();
|
|
133
|
+
} finally {
|
|
134
|
+
await file.close().catch(() => {});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isGunzipOutputLimitError(error: unknown): boolean {
|
|
139
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ERR_BUFFER_TOO_LARGE";
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
interface ResolvedSource {
|
|
99
143
|
dir: string;
|
|
100
144
|
source: GjcPluginRegistrySource;
|
|
@@ -136,19 +180,15 @@ function tarHeaderChecksumOk(header: Uint8Array): boolean {
|
|
|
136
180
|
|
|
137
181
|
/** Minimal, traversal/symlink-safe, resource-bounded extraction of a tar(.gz). */
|
|
138
182
|
async function extractTarball(tarPath: string, destRoot: string): Promise<void> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// other unreachable source, instead of a raw errno escaping the lifecycle.
|
|
142
|
-
let raw: Buffer;
|
|
143
|
-
try {
|
|
144
|
-
raw = await fs.readFile(tarPath);
|
|
145
|
-
} catch {
|
|
146
|
-
throw new GjcPluginSourceUnavailableError();
|
|
147
|
-
}
|
|
183
|
+
const isGzip = /\.(tgz|tar\.gz)$/i.test(tarPath);
|
|
184
|
+
const raw = await readTarballInput(tarPath, isGzip ? TAR_MAX_COMPRESSED_BYTES : TAR_MAX_ARCHIVE_BYTES);
|
|
148
185
|
let buf: Buffer;
|
|
149
186
|
try {
|
|
150
|
-
buf =
|
|
151
|
-
} catch {
|
|
187
|
+
buf = isGzip ? gunzipSync(raw, { maxOutputLength: TAR_MAX_ARCHIVE_BYTES }) : raw;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (isGunzipOutputLimitError(error)) {
|
|
190
|
+
throw new GjcPluginLoadError("security_policy", "GJC plugin tarball exceeds the expansion limit");
|
|
191
|
+
}
|
|
152
192
|
throw new GjcPluginLoadError("invalid_manifest", "GJC plugin tarball could not be decompressed");
|
|
153
193
|
}
|
|
154
194
|
const resolvedRoot = path.resolve(destRoot);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getTrustedHomeDir } from "@gajae-code/utils";
|
|
4
|
-
import { findRepoRoot } from "../capability/fs";
|
|
3
|
+
import { getPluginsDir, getTrustedHomeDir } from "@gajae-code/utils";
|
|
4
|
+
import { findRepoRoot, invalidate as invalidateFsCache } from "../capability/fs";
|
|
5
5
|
import type { Skill as CapabilitySkill } from "../capability/skill";
|
|
6
6
|
import type { SkillsSettings } from "../config/settings-schema";
|
|
7
7
|
import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
|
|
8
8
|
import { scanClaudeProjectSkills, scanClaudeUserSkills } from "../discovery/claude";
|
|
9
9
|
import { scanCodexProjectSkills, scanCodexUserSkills } from "../discovery/codex";
|
|
10
|
-
import { compareSkillOrder, SOURCE_PATHS, scanSkillsFromDir } from "../discovery/helpers";
|
|
10
|
+
import { compareSkillOrder, listClaudePluginRoots, SOURCE_PATHS, scanSkillsFromDir } from "../discovery/helpers";
|
|
11
11
|
import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
|
|
12
12
|
import { expandTilde } from "../tools/path-utils";
|
|
13
13
|
import type { Skill } from "./skills";
|
|
@@ -234,6 +234,53 @@ function pushDiagnostic(diagnostics: string[], message: string): void {
|
|
|
234
234
|
if (diagnostics.length < MAX_DIAGNOSTICS) diagnostics.push(message);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
async function collectPluginSkillDirs(
|
|
238
|
+
home: string,
|
|
239
|
+
cwd: string,
|
|
240
|
+
): Promise<Array<{ dir: string; pluginName: string; level: "user" | "project" }>> {
|
|
241
|
+
// Ensure the underlying installed_plugins.json fs cache is current — install/upgrade
|
|
242
|
+
// writers already invalidate via clearPluginRootsAndCaches; this is a defensive
|
|
243
|
+
// fallback so a stale in-memory cache cannot mask a disabled/removed plugin.
|
|
244
|
+
invalidateFsCache(`${getPluginsDir(home)}/installed_plugins.json`);
|
|
245
|
+
try {
|
|
246
|
+
const { roots } = await listClaudePluginRoots(home, cwd);
|
|
247
|
+
return roots.map(root => ({
|
|
248
|
+
dir: `${root.path}/skills`,
|
|
249
|
+
pluginName: root.plugin,
|
|
250
|
+
level: root.scope,
|
|
251
|
+
}));
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function diagnoseCustomDir(expandedDir: string, diagnostics: string[]): Promise<boolean> {
|
|
258
|
+
try {
|
|
259
|
+
const stat = await fs.lstat(expandedDir);
|
|
260
|
+
if (stat.isSymbolicLink()) {
|
|
261
|
+
try {
|
|
262
|
+
await fs.stat(expandedDir);
|
|
263
|
+
} catch {
|
|
264
|
+
pushDiagnostic(
|
|
265
|
+
diagnostics,
|
|
266
|
+
`skills.customDirectories entry "${expandedDir}" is a dangling symlink (target missing, likely a purged plugin cache dir); fix or remove it`,
|
|
267
|
+
);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
274
|
+
pushDiagnostic(
|
|
275
|
+
diagnostics,
|
|
276
|
+
`skills.customDirectories entry "${expandedDir}" does not exist (missing directory, likely a purged plugin cache dir); fix or remove it`,
|
|
277
|
+
);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
237
284
|
async function scanProjectOrUserDir(
|
|
238
285
|
ctx: { cwd: string; home: string; repoRoot: string | null },
|
|
239
286
|
dir: string,
|
|
@@ -376,6 +423,44 @@ export async function discoverRuntimeSkills(
|
|
|
376
423
|
}
|
|
377
424
|
}
|
|
378
425
|
|
|
426
|
+
// Marketplace plugin skills: version-stable, enabled-only.
|
|
427
|
+
// Uses the same trust gate as native user/project scopes and the same
|
|
428
|
+
// discovery root (<installPath>/skills) the capability provider uses,
|
|
429
|
+
// namespaced as `plugin:skillName`.
|
|
430
|
+
if (policy?.enabled === true) {
|
|
431
|
+
const pluginDirs = await collectPluginSkillDirs(home, options.cwd);
|
|
432
|
+
for (const entry of pluginDirs) {
|
|
433
|
+
if (entry.level === "project" && !(source === "all" || source === "project")) continue;
|
|
434
|
+
if (entry.level === "user" && !(source === "all" || source === "user")) continue;
|
|
435
|
+
if (!sourceEnabled(entry.level, policy)) continue;
|
|
436
|
+
const label = `plugin ${entry.pluginName} skills`;
|
|
437
|
+
scanJobs.push(
|
|
438
|
+
(async () => {
|
|
439
|
+
const pluginCtx = { cwd: options.cwd, home, repoRoot: home };
|
|
440
|
+
const result = await scanSkillsFromDir(pluginCtx, {
|
|
441
|
+
dir: entry.dir,
|
|
442
|
+
providerId: "plugin",
|
|
443
|
+
level: entry.level,
|
|
444
|
+
requireDescription: true,
|
|
445
|
+
});
|
|
446
|
+
for (const skill of result.items) {
|
|
447
|
+
skill.name = `${entry.pluginName}:${skill.name}`;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
items: result.items.map(skill => ({ skill, source: entry.level as RuntimeSkillDiscoverySource })),
|
|
451
|
+
warnings: result.warnings ?? [],
|
|
452
|
+
label,
|
|
453
|
+
};
|
|
454
|
+
})(),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (policy?.enabled === true && policy.customDirectories && policy.customDirectories.length > 0) {
|
|
459
|
+
for (const dir of getCustomSkillDirs(policy, home)) {
|
|
460
|
+
await diagnoseCustomDir(dir, diagnostics);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
379
464
|
const settled = await Promise.all(scanJobs.map(job => job.catch(error => ({ error: String(error), label: "" }))));
|
|
380
465
|
|
|
381
466
|
const seenNames = new Set<string>();
|
|
@@ -470,6 +555,28 @@ export async function findRuntimeSkillByName(
|
|
|
470
555
|
);
|
|
471
556
|
}
|
|
472
557
|
}
|
|
558
|
+
if (policy?.enabled === true) {
|
|
559
|
+
try {
|
|
560
|
+
const pluginDirs = await collectPluginSkillDirs(home, cwd);
|
|
561
|
+
for (const entry of pluginDirs) {
|
|
562
|
+
if (!sourceEnabled(entry.level, policy)) continue;
|
|
563
|
+
scanJobs.push(
|
|
564
|
+
scanSkillsFromDir(
|
|
565
|
+
{ cwd, home, repoRoot: home },
|
|
566
|
+
{
|
|
567
|
+
dir: entry.dir,
|
|
568
|
+
providerId: "plugin",
|
|
569
|
+
level: entry.level,
|
|
570
|
+
requireDescription: true,
|
|
571
|
+
},
|
|
572
|
+
).then(result => {
|
|
573
|
+
for (const skill of result.items) skill.name = `${entry.pluginName}:${skill.name}`;
|
|
574
|
+
return result.items.map(skill => ({ skill, source: entry.level as RuntimeSkillDiscoverySource }));
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
} catch {}
|
|
579
|
+
}
|
|
473
580
|
for (const entry of (await Promise.all(scanJobs)).flat()) {
|
|
474
581
|
if (entry.skill.name === normalized && isAllowedByPolicy(entry.skill, policy, [])) {
|
|
475
582
|
return toRuntimeSkill(entry.skill, entry.source);
|
|
@@ -122,7 +122,7 @@ export interface LoadSkillsOptions extends SkillsSettings {
|
|
|
122
122
|
*/
|
|
123
123
|
const LOADABLE_SKILL_PROVIDERS = new Set(["native"]);
|
|
124
124
|
|
|
125
|
-
const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
|
|
125
|
+
const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS.map(name => name.toLowerCase()));
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
128
|
* Load skills from all configured locations.
|
|
@@ -235,21 +235,22 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
235
235
|
// workflow-routing hijack: warn, and let the session merge keep the bundled
|
|
236
236
|
// definition authoritative (sdk/session.ts). User-scope copies are not
|
|
237
237
|
// warned about: installed defaults legitimately mirror bundled skills.
|
|
238
|
-
if (capSkill.level === "project" && BUILT_IN_SKILL_NAMES.has(capSkill.name)) {
|
|
238
|
+
if (capSkill.level === "project" && BUILT_IN_SKILL_NAMES.has(capSkill.name.toLowerCase())) {
|
|
239
239
|
collisionWarnings.push({
|
|
240
240
|
skillPath: capSkill.path,
|
|
241
241
|
message: `name collision: "${capSkill.name}" is a bundled GJC workflow skill; the bundled definition takes precedence in sessions and this project copy is never used`,
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
const
|
|
245
|
+
const skillKey = capSkill.name.toLowerCase();
|
|
246
|
+
const existing = skillMap.get(skillKey);
|
|
246
247
|
if (existing) {
|
|
247
248
|
collisionWarnings.push({
|
|
248
249
|
skillPath: capSkill.path,
|
|
249
250
|
message: `name collision: "${capSkill.name}" already loaded from ${existing.filePath}, skipping this one`,
|
|
250
251
|
});
|
|
251
252
|
} else {
|
|
252
|
-
skillMap.set(
|
|
253
|
+
skillMap.set(skillKey, {
|
|
253
254
|
name: capSkill.name,
|
|
254
255
|
description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
|
|
255
256
|
filePath: capSkill.path,
|
|
@@ -318,14 +319,15 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
318
319
|
const resolvedPath = customRealPaths[i];
|
|
319
320
|
if (realPathSet.has(resolvedPath)) continue;
|
|
320
321
|
|
|
321
|
-
const
|
|
322
|
+
const skillKey = skill.name.toLowerCase();
|
|
323
|
+
const existing = skillMap.get(skillKey);
|
|
322
324
|
if (existing) {
|
|
323
325
|
collisionWarnings.push({
|
|
324
326
|
skillPath: skill.filePath,
|
|
325
327
|
message: `name collision: "${skill.name}" already loaded from ${existing.filePath}, skipping this one`,
|
|
326
328
|
});
|
|
327
329
|
} else {
|
|
328
|
-
skillMap.set(
|
|
330
|
+
skillMap.set(skillKey, skill);
|
|
329
331
|
realPathSet.add(resolvedPath);
|
|
330
332
|
}
|
|
331
333
|
}
|
|
@@ -358,7 +360,7 @@ export function getSkillSlashCommandName(skill: Pick<Skill, "name">): string {
|
|
|
358
360
|
}
|
|
359
361
|
|
|
360
362
|
export function isNamespacedSkillSlashCommandName(commandName: string): boolean {
|
|
361
|
-
return commandName.startsWith("skill:");
|
|
363
|
+
return commandName.toLowerCase().startsWith("skill:");
|
|
362
364
|
}
|
|
363
365
|
|
|
364
366
|
export function getSkillSlashCommandNames(skill: Pick<Skill, "name">): string[] {
|
|
@@ -366,7 +368,7 @@ export function getSkillSlashCommandNames(skill: Pick<Skill, "name">): string[]
|
|
|
366
368
|
}
|
|
367
369
|
|
|
368
370
|
export function isSkillSlashCommandName(commandName: string, skill: Pick<Skill, "name">): boolean {
|
|
369
|
-
return commandName === getSkillSlashCommandName(skill);
|
|
371
|
+
return commandName.toLowerCase() === getSkillSlashCommandName(skill).toLowerCase();
|
|
370
372
|
}
|
|
371
373
|
|
|
372
374
|
export interface ResolvedSkillSlashCommand {
|
|
@@ -410,12 +412,19 @@ export function parseSkillInvocations(
|
|
|
410
412
|
): ParsedSkillInvocation[] {
|
|
411
413
|
const trimmedText = text.trim();
|
|
412
414
|
if (!trimmedText) return [];
|
|
413
|
-
const
|
|
415
|
+
const normalizedSkillsByCommandName = new Map<string, Skill>();
|
|
416
|
+
for (const [commandName, skill] of skillsByCommandName) {
|
|
417
|
+
const normalizedCommandName = commandName.toLowerCase();
|
|
418
|
+
if (!normalizedSkillsByCommandName.has(normalizedCommandName)) {
|
|
419
|
+
normalizedSkillsByCommandName.set(normalizedCommandName, skill);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const canonicalSkillCommandPattern = /(^|\s)\/(skill:[^\s]+)/gi;
|
|
414
423
|
const matches: SkillTokenMatch[] = [];
|
|
415
424
|
for (const match of trimmedText.matchAll(canonicalSkillCommandPattern)) {
|
|
416
425
|
const commandName = match[2];
|
|
417
426
|
if (!commandName) continue;
|
|
418
|
-
const skill =
|
|
427
|
+
const skill = normalizedSkillsByCommandName.get(commandName.toLowerCase());
|
|
419
428
|
if (!skill) continue;
|
|
420
429
|
const leading = match[1] ?? "";
|
|
421
430
|
const index = (match.index ?? 0) + leading.length;
|
|
@@ -458,10 +467,11 @@ export function resolveSkillSlashCommands(
|
|
|
458
467
|
for (const skill of skills) {
|
|
459
468
|
if (skill.hide === true) continue;
|
|
460
469
|
for (const name of getSkillSlashCommandNames(skill)) {
|
|
461
|
-
|
|
470
|
+
const normalizedName = name.toLowerCase();
|
|
471
|
+
if (claimedNames.has(normalizedName)) {
|
|
462
472
|
continue;
|
|
463
473
|
}
|
|
464
|
-
claimedNames.add(
|
|
474
|
+
claimedNames.add(normalizedName);
|
|
465
475
|
commands.push({ name, description: skill.description, skill });
|
|
466
476
|
}
|
|
467
477
|
}
|