@gajae-code/coding-agent 0.3.2 → 0.4.1
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 +39 -0
- package/dist/types/config/model-registry.d.ts +17 -10
- package/dist/types/config/models-config-schema.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +5 -0
- package/dist/types/edit/diff.d.ts +16 -0
- package/dist/types/edit/modes/replace.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
- package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
- package/dist/types/extensibility/skills.d.ts +9 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
- package/dist/types/harness-control-plane/storage.d.ts +7 -0
- package/dist/types/lsp/client.d.ts +1 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/blob-store.d.ts +17 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/skill-state/active-state.d.ts +13 -0
- package/dist/types/thinking.d.ts +3 -2
- package/dist/types/tools/index.d.ts +3 -0
- package/package.json +9 -7
- package/src/cli.ts +14 -0
- package/src/commands/harness.ts +192 -7
- package/src/commands/ultragoal.ts +1 -21
- package/src/config/model-equivalence.ts +1 -1
- package/src/config/model-registry.ts +32 -5
- package/src/config/models-config-schema.ts +7 -2
- package/src/config/settings-schema.ts +4 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
- package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
- package/src/discovery/claude-plugins.ts +25 -5
- package/src/edit/diff.ts +64 -1
- package/src/edit/modes/replace.ts +60 -2
- package/src/extensibility/gjc-plugins/activation.ts +87 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +114 -0
- package/src/extensibility/gjc-plugins/loader.ts +131 -0
- package/src/extensibility/gjc-plugins/paths.ts +66 -0
- package/src/extensibility/gjc-plugins/schema.ts +79 -0
- package/src/extensibility/gjc-plugins/state.ts +29 -0
- package/src/extensibility/gjc-plugins/tools.ts +47 -0
- package/src/extensibility/gjc-plugins/types.ts +97 -0
- package/src/extensibility/gjc-plugins/validation.ts +76 -0
- package/src/extensibility/skills.ts +39 -7
- package/src/gjc-runtime/state-runtime.ts +93 -2
- package/src/gjc-runtime/state-writer.ts +17 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
- package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
- package/src/gjc-runtime/workflow-manifest.ts +2 -2
- package/src/harness-control-plane/storage.ts +144 -2
- package/src/hashline/hash.ts +23 -0
- package/src/hooks/skill-state.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +7 -0
- package/src/modes/acp/acp-agent.ts +25 -2
- package/src/modes/bridge/bridge-mode.ts +124 -2
- package/src/modes/controllers/input-controller.ts +14 -2
- package/src/modes/prompt-action-autocomplete.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +79 -3
- package/src/modes/rpc/rpc-mode.ts +67 -0
- package/src/modes/rpc/rpc-types.ts +224 -2
- package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
- package/src/modes/shared/agent-wire/command-validation.ts +25 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
- package/src/modes/shared/agent-wire/handshake.ts +43 -3
- package/src/modes/shared/agent-wire/protocol.ts +7 -0
- package/src/modes/shared/agent-wire/responses.ts +2 -2
- package/src/modes/shared/agent-wire/scopes.ts +2 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/system/system-prompt.md +9 -0
- package/src/runtime-mcp/client.ts +7 -4
- package/src/runtime-mcp/manager.ts +45 -13
- package/src/runtime-mcp/transports/http.ts +40 -14
- package/src/runtime-mcp/transports/stdio.ts +11 -10
- package/src/sdk.ts +47 -0
- package/src/session/agent-session.ts +211 -2
- package/src/session/blob-store.ts +84 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-manager.ts +390 -33
- package/src/session/session-storage.ts +26 -0
- package/src/setup/provider-onboarding.ts +2 -2
- package/src/skill-state/active-state.ts +89 -1
- package/src/task/discovery.ts +7 -1
- package/src/task/executor.ts +16 -2
- package/src/thinking.ts +8 -2
- package/src/tools/ask.ts +39 -9
- package/src/tools/index.ts +3 -0
- package/src/tools/skill.ts +15 -3
- package/src/utils/edit-mode.ts +1 -1
package/src/lsp/client.ts
CHANGED
|
@@ -61,6 +61,10 @@ function stopIdleChecker(): void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export function isIdleCheckerActiveForTests(): boolean {
|
|
65
|
+
return idleCheckInterval !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
// =============================================================================
|
|
65
69
|
// Client Capabilities
|
|
66
70
|
// =============================================================================
|
|
@@ -919,6 +923,9 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
919
923
|
* Shutdown all LSP clients.
|
|
920
924
|
*/
|
|
921
925
|
export async function shutdownAll(): Promise<void> {
|
|
926
|
+
stopIdleChecker();
|
|
927
|
+
clientLocks.clear();
|
|
928
|
+
fileOperationLocks.clear();
|
|
922
929
|
const clientsToShutdown = Array.from(clients.values());
|
|
923
930
|
clients.clear();
|
|
924
931
|
await Promise.allSettled(clientsToShutdown.map(client => shutdownClientInstance(client)));
|
|
@@ -50,6 +50,7 @@ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../
|
|
|
50
50
|
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../extensibility/extensions";
|
|
51
51
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
52
52
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
53
|
+
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
|
|
53
54
|
import {
|
|
54
55
|
buildSkillPromptMessage,
|
|
55
56
|
getSkillSlashCommandNames,
|
|
@@ -722,7 +723,18 @@ export class AcpAgent implements Agent {
|
|
|
722
723
|
for (let index = 0; index < invocations.length; index += 1) {
|
|
723
724
|
const invocation = invocations[index];
|
|
724
725
|
if (!invocation) continue;
|
|
725
|
-
const
|
|
726
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
727
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
728
|
+
sessionId: record.session.sessionId,
|
|
729
|
+
skillName: invocation.skill.name,
|
|
730
|
+
args: invocation.args,
|
|
731
|
+
});
|
|
732
|
+
const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
|
|
733
|
+
subskillActivation: activationResult.activation,
|
|
734
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
735
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
736
|
+
sessionId: record.session.sessionId,
|
|
737
|
+
});
|
|
726
738
|
if (index === invocations.length - 1) {
|
|
727
739
|
await record.session.promptCustomMessage({
|
|
728
740
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
@@ -757,7 +769,18 @@ export class AcpAgent implements Agent {
|
|
|
757
769
|
if (!skill || skill.hide === true) {
|
|
758
770
|
return false;
|
|
759
771
|
}
|
|
760
|
-
const
|
|
772
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
773
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
774
|
+
sessionId: record.session.sessionId,
|
|
775
|
+
skillName: skill.name,
|
|
776
|
+
args,
|
|
777
|
+
});
|
|
778
|
+
const built = await buildSkillPromptMessage(skill, activationResult.cleanedArgs, {
|
|
779
|
+
subskillActivation: activationResult.activation,
|
|
780
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
781
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
782
|
+
sessionId: record.session.sessionId,
|
|
783
|
+
});
|
|
761
784
|
await record.session.promptCustomMessage({
|
|
762
785
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
763
786
|
content: built.message,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { ExtensionUIContext } from "../../extensibility/extensions";
|
|
2
3
|
import type { AgentSession } from "../../session/agent-session";
|
|
3
4
|
import type { ClientBridgePermissionOutcome } from "../../session/client-bridge";
|
|
4
|
-
import type { RpcCommand, RpcResponse } from "../rpc/rpc-types";
|
|
5
|
+
import type { RpcCommand, RpcResponse, RpcWorkflowGateResponse } from "../rpc/rpc-types";
|
|
5
6
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
6
7
|
import { isRpcCommand } from "../shared/agent-wire/command-validation";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
BridgeFrameSequencer,
|
|
10
|
+
toBridgeEventFrame,
|
|
11
|
+
toBridgeWorkflowGateFrame,
|
|
12
|
+
} from "../shared/agent-wire/event-envelope";
|
|
8
13
|
import type { BridgeCapability } from "../shared/agent-wire/handshake";
|
|
9
14
|
import {
|
|
10
15
|
type BridgeHandshakeRequest,
|
|
@@ -23,6 +28,9 @@ import {
|
|
|
23
28
|
} from "../shared/agent-wire/scopes";
|
|
24
29
|
import { UiRequestBroker } from "../shared/agent-wire/ui-request-broker";
|
|
25
30
|
import type { BridgeUiResult } from "../shared/agent-wire/ui-result";
|
|
31
|
+
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
32
|
+
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
33
|
+
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
26
34
|
import { assertSafeBridgeBind, isBridgeTokenAuthorized } from "./auth";
|
|
27
35
|
import { type BridgePermissionRequestPayload, createBridgeClientBridge } from "./bridge-client-bridge";
|
|
28
36
|
import { BridgeExtensionUIContext, type BridgeUiRequestPayload } from "./bridge-ui-context";
|
|
@@ -39,6 +47,7 @@ const SERVER_CAPABILITIES: readonly BridgeCapability[] = [
|
|
|
39
47
|
"ui.declarative",
|
|
40
48
|
"host_tools",
|
|
41
49
|
"host_uri",
|
|
50
|
+
"workflow_gate",
|
|
42
51
|
];
|
|
43
52
|
|
|
44
53
|
const DEFAULT_BRIDGE_SCOPES: readonly BridgeCommandScope[] = ["prompt"];
|
|
@@ -71,6 +80,7 @@ const SERVER_FRAME_TYPES: readonly BridgeFrameType[] = [
|
|
|
71
80
|
"host_tool_call",
|
|
72
81
|
"host_uri_request",
|
|
73
82
|
"reset",
|
|
83
|
+
"workflow_gate",
|
|
74
84
|
"error",
|
|
75
85
|
];
|
|
76
86
|
|
|
@@ -86,6 +96,7 @@ interface BridgeFetchHandlerOptions {
|
|
|
86
96
|
hostToolBridge?: RpcHostToolBridge;
|
|
87
97
|
hostUriBridge?: RpcHostUriBridge;
|
|
88
98
|
endpointMatrix?: Partial<BridgeEndpointMatrix>;
|
|
99
|
+
unattendedControlPlane?: UnattendedSessionControlPlane;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
interface BridgeIdempotencyRecord {
|
|
@@ -174,6 +185,15 @@ function bridgeHelpResponse(matrix: BridgeEndpointMatrix): Response {
|
|
|
174
185
|
});
|
|
175
186
|
}
|
|
176
187
|
|
|
188
|
+
function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
|
|
189
|
+
if (event.includes("denied")) return "denied";
|
|
190
|
+
if (event.includes("exceeded")) return "exceeded";
|
|
191
|
+
if (event.includes("abort")) return "aborted";
|
|
192
|
+
if (event.includes("rejected") || event.includes("conflict")) return "rejected";
|
|
193
|
+
if (event.includes("accepted") || event.includes("negotiated") || event.includes("emitted")) return "accepted";
|
|
194
|
+
return "info";
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
function frameTypeForDispatchOutput(obj: RpcResponse | object): BridgeFrameType {
|
|
178
198
|
const type = typeof obj === "object" && obj !== null && "type" in obj ? (obj as { type?: unknown }).type : undefined;
|
|
179
199
|
if (type === "host_tool_call" || type === "host_tool_cancel") return "host_tool_call";
|
|
@@ -219,6 +239,24 @@ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (r
|
|
|
219
239
|
if (!isBridgeHandshakeRequest(payload)) {
|
|
220
240
|
return jsonResponse(400, { error: "invalid_request" });
|
|
221
241
|
}
|
|
242
|
+
let acceptedUnattended = options.unattendedControlPlane?.isUnattended() ? payload.unattended : undefined;
|
|
243
|
+
if (
|
|
244
|
+
acceptedUnattended === undefined &&
|
|
245
|
+
payload.unattended !== undefined &&
|
|
246
|
+
endpointMatrix.events &&
|
|
247
|
+
options.unattendedControlPlane
|
|
248
|
+
) {
|
|
249
|
+
try {
|
|
250
|
+
options.unattendedControlPlane.negotiate(payload.unattended);
|
|
251
|
+
acceptedUnattended = payload.unattended;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
const error =
|
|
254
|
+
err instanceof Error && "code" in err
|
|
255
|
+
? { code: (err as { code: unknown }).code, message: err.message }
|
|
256
|
+
: { error: err instanceof Error ? err.message : String(err) };
|
|
257
|
+
return jsonResponse(403, error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
222
260
|
return jsonResponse(
|
|
223
261
|
200,
|
|
224
262
|
negotiateBridgeHandshake(payload, {
|
|
@@ -243,6 +281,7 @@ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (r
|
|
|
243
281
|
: "",
|
|
244
282
|
},
|
|
245
283
|
frameTypes: endpointMatrix.events ? SERVER_FRAME_TYPES : [],
|
|
284
|
+
acceptedUnattended,
|
|
246
285
|
}),
|
|
247
286
|
);
|
|
248
287
|
}
|
|
@@ -360,6 +399,36 @@ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (r
|
|
|
360
399
|
} catch {
|
|
361
400
|
return jsonResponse(400, { error: "invalid_json" });
|
|
362
401
|
}
|
|
402
|
+
if (
|
|
403
|
+
payload !== null &&
|
|
404
|
+
typeof payload === "object" &&
|
|
405
|
+
"gate_id" in payload &&
|
|
406
|
+
"answer" in payload &&
|
|
407
|
+
(correlationId === (payload as RpcWorkflowGateResponse).gate_id || correlationId.startsWith("wg_"))
|
|
408
|
+
) {
|
|
409
|
+
try {
|
|
410
|
+
const resolution = await options.unattendedControlPlane?.resolveGate({
|
|
411
|
+
gate_id: (payload as RpcWorkflowGateResponse).gate_id,
|
|
412
|
+
answer: (payload as RpcWorkflowGateResponse).answer,
|
|
413
|
+
idempotency_key: (payload as RpcWorkflowGateResponse).idempotency_key ?? idempotencyKey,
|
|
414
|
+
});
|
|
415
|
+
if (resolution) {
|
|
416
|
+
rememberIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
|
|
417
|
+
route: url.pathname,
|
|
418
|
+
ownerToken,
|
|
419
|
+
body,
|
|
420
|
+
response: resolution,
|
|
421
|
+
});
|
|
422
|
+
return jsonResponse(200, resolution);
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const error =
|
|
426
|
+
err instanceof Error && "code" in err
|
|
427
|
+
? { code: (err as { code: unknown }).code, message: err.message }
|
|
428
|
+
: { error: err instanceof Error ? err.message : String(err) };
|
|
429
|
+
return jsonResponse(409, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
363
432
|
const permissionResult = options.permissionBroker?.respond(
|
|
364
433
|
correlationId,
|
|
365
434
|
ownerToken,
|
|
@@ -490,6 +559,57 @@ export async function runBridgeMode(
|
|
|
490
559
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
491
560
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
492
561
|
const idempotencyCache: BridgeIdempotencyCache = new Map();
|
|
562
|
+
const auditLog = new UnattendedAuditLog(defaultAuditPath(session.sessionId, session.sessionManager.getCwd()), {
|
|
563
|
+
redactAnswers: true,
|
|
564
|
+
});
|
|
565
|
+
const recordAudit = (event: { event: string; [key: string]: unknown }) => {
|
|
566
|
+
const payload =
|
|
567
|
+
typeof event.payload === "object" && event.payload !== null
|
|
568
|
+
? (event.payload as Record<string, unknown>)
|
|
569
|
+
: undefined;
|
|
570
|
+
const gateId =
|
|
571
|
+
typeof event.gate_id === "string"
|
|
572
|
+
? event.gate_id
|
|
573
|
+
: typeof payload?.gate_id === "string"
|
|
574
|
+
? payload.gate_id
|
|
575
|
+
: undefined;
|
|
576
|
+
auditLog.record({
|
|
577
|
+
run_id: session.sessionId,
|
|
578
|
+
session_id: session.sessionId,
|
|
579
|
+
actor: typeof event.actor === "string" ? event.actor : undefined,
|
|
580
|
+
event: event.event,
|
|
581
|
+
outcome: auditOutcomeFor(event.event),
|
|
582
|
+
dedupe_key: `${event.event}:${gateId ?? "run"}:${JSON.stringify(payload ?? event)}`,
|
|
583
|
+
gate_id: gateId,
|
|
584
|
+
stage: typeof event.stage === "string" ? (event.stage as never) : undefined,
|
|
585
|
+
kind: typeof event.kind === "string" ? (event.kind as never) : undefined,
|
|
586
|
+
scope: typeof payload?.scope === "string" ? payload.scope : undefined,
|
|
587
|
+
action: typeof payload?.action === "string" ? payload.action : undefined,
|
|
588
|
+
budget: event.event === "budget_exceeded" ? (payload as never) : undefined,
|
|
589
|
+
answer_hash: typeof event.answer_hash === "string" ? event.answer_hash : undefined,
|
|
590
|
+
error: payload && event.event.endsWith("denied") ? payload : undefined,
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
const gateStore = new FileGateStore(
|
|
594
|
+
path.join(session.sessionManager.getCwd(), ".gjc", "state", "workflow-gates", `${session.sessionId}.json`),
|
|
595
|
+
);
|
|
596
|
+
const unattendedControlPlane = new UnattendedSessionControlPlane({
|
|
597
|
+
runId: session.sessionId,
|
|
598
|
+
sessionId: session.sessionId,
|
|
599
|
+
emitFrame: gate => eventStream.publish(toBridgeWorkflowGateFrame(gate, sequencer)),
|
|
600
|
+
store: gateStore,
|
|
601
|
+
audit: recordAudit,
|
|
602
|
+
getUsageSnapshot: () => {
|
|
603
|
+
const stats = session.getSessionStats();
|
|
604
|
+
return { tokens: stats.tokens.total, costUsd: stats.cost };
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
session.setWorkflowGateEmitter(unattendedControlPlane);
|
|
608
|
+
unattendedControlPlane
|
|
609
|
+
.recover()
|
|
610
|
+
.catch(err =>
|
|
611
|
+
eventStream.publish(sequencer.next("error", { error: err instanceof Error ? err.message : String(err) })),
|
|
612
|
+
);
|
|
493
613
|
|
|
494
614
|
Bun.serve({
|
|
495
615
|
hostname,
|
|
@@ -505,6 +625,7 @@ export async function runBridgeMode(
|
|
|
505
625
|
hostToolBridge,
|
|
506
626
|
hostUriBridge,
|
|
507
627
|
commandScopes,
|
|
628
|
+
unattendedControlPlane,
|
|
508
629
|
commandDispatcher: command =>
|
|
509
630
|
dispatchRpcCommand(command, {
|
|
510
631
|
session,
|
|
@@ -512,6 +633,7 @@ export async function runBridgeMode(
|
|
|
512
633
|
hostToolRegistry: hostToolBridge,
|
|
513
634
|
hostUriRegistry: hostUriBridge,
|
|
514
635
|
createUiContext: () => uiContext,
|
|
636
|
+
unattendedControlPlane,
|
|
515
637
|
}),
|
|
516
638
|
}),
|
|
517
639
|
});
|
|
@@ -4,6 +4,7 @@ import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
|
4
4
|
import type { AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
|
|
5
5
|
import { $env, sanitizeText } from "@gajae-code/utils";
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
|
+
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
|
|
7
8
|
import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
|
|
8
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
9
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
@@ -472,9 +473,20 @@ export class InputController {
|
|
|
472
473
|
for (let index = 0; index < invocations.length; index += 1) {
|
|
473
474
|
const invocation = invocations[index];
|
|
474
475
|
if (!invocation) continue;
|
|
475
|
-
const
|
|
476
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
477
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
478
|
+
sessionId: this.ctx.session.sessionId,
|
|
479
|
+
skillName: invocation.skill.name,
|
|
480
|
+
args: invocation.args,
|
|
481
|
+
});
|
|
482
|
+
const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
|
|
483
|
+
subskillActivation: activationResult.activation,
|
|
484
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
485
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
486
|
+
sessionId: this.ctx.session.sessionId,
|
|
487
|
+
});
|
|
476
488
|
const details: SkillPromptDetails = built.details;
|
|
477
|
-
const displayText = `/${invocation.commandName}${
|
|
489
|
+
const displayText = `/${invocation.commandName}${activationResult.cleanedArgs ? ` ${activationResult.cleanedArgs}` : ""}`;
|
|
478
490
|
// When the agent is streaming, register a compact slash-form text as
|
|
479
491
|
// the pending-display twin BEFORE dispatching the CustomMessage. The
|
|
480
492
|
// returned tag is embedded in details so AgentSession.#handleAgentEvent
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
getKeybindings,
|
|
6
|
-
type SlashCommand,
|
|
7
|
-
} from "@gajae-code/tui";
|
|
8
|
-
import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
|
|
1
|
+
import type { AutocompleteItem, AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
|
|
2
|
+
import { CombinedAutocompleteProvider, getKeybindings, getSlashCommandMatchRank } from "@gajae-code/tui";
|
|
3
|
+
import type { KeybindingsManager } from "../config/keybindings";
|
|
4
|
+
import { formatKeyHints } from "../config/keybindings";
|
|
9
5
|
import { isSettingsInitialized, settings } from "../config/settings";
|
|
10
6
|
import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
|
|
11
7
|
|
|
@@ -104,6 +100,43 @@ function mergeAutocompleteSuggestions(
|
|
|
104
100
|
return { items, prefix: primary.prefix };
|
|
105
101
|
}
|
|
106
102
|
|
|
103
|
+
function sortSlashCommandSuggestions(
|
|
104
|
+
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
105
|
+
commands: SlashCommand[],
|
|
106
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
107
|
+
if (!suggestions) return null;
|
|
108
|
+
const query = suggestions.prefix.slice(1).toLowerCase();
|
|
109
|
+
const commandIndexes = new Map(commands.map((command, index) => [command.name, index]));
|
|
110
|
+
const commandByName = new Map(commands.map(command => [command.name, command]));
|
|
111
|
+
const items = suggestions.items
|
|
112
|
+
.map((item, index) => {
|
|
113
|
+
const command = commandByName.get(item.value);
|
|
114
|
+
const commandIndex = commandIndexes.get(item.value) ?? index;
|
|
115
|
+
const lowerName = item.value.toLowerCase();
|
|
116
|
+
const lowerDesc = command?.description?.toLowerCase() ?? item.description?.toLowerCase() ?? "";
|
|
117
|
+
const nameScore = fuzzyMatch(query, lowerName) ? fuzzyScore(query, lowerName) : 0;
|
|
118
|
+
const descScore = fuzzyMatch(query, lowerDesc) ? fuzzyScore(query, lowerDesc) * 0.5 : 0;
|
|
119
|
+
return {
|
|
120
|
+
item,
|
|
121
|
+
index,
|
|
122
|
+
commandIndex,
|
|
123
|
+
matchRank: getSlashCommandMatchRank(query, lowerName),
|
|
124
|
+
priority: command?.priority ?? 0,
|
|
125
|
+
score: Math.max(nameScore, descScore),
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
.sort(
|
|
129
|
+
(a, b) =>
|
|
130
|
+
a.matchRank - b.matchRank ||
|
|
131
|
+
b.priority - a.priority ||
|
|
132
|
+
b.score - a.score ||
|
|
133
|
+
a.commandIndex - b.commandIndex ||
|
|
134
|
+
a.index - b.index,
|
|
135
|
+
)
|
|
136
|
+
.map(({ item }) => item);
|
|
137
|
+
return { ...suggestions, items };
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
function withoutSkillCommandSuggestions(
|
|
108
141
|
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
109
142
|
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
@@ -182,7 +215,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
182
215
|
await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol),
|
|
183
216
|
);
|
|
184
217
|
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
|
|
185
|
-
return
|
|
218
|
+
return sortSlashCommandSuggestions(
|
|
219
|
+
mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
|
|
220
|
+
this.#commands,
|
|
221
|
+
);
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
|
|
@@ -253,7 +289,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
253
289
|
this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null,
|
|
254
290
|
);
|
|
255
291
|
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
|
|
256
|
-
return
|
|
292
|
+
return sortSlashCommandSuggestions(
|
|
293
|
+
mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
|
|
294
|
+
this.#commands,
|
|
295
|
+
);
|
|
257
296
|
}
|
|
258
297
|
trySyncInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
|
|
259
298
|
if (isSettingsInitialized() && !settings.get("emojiAutocomplete")) return null;
|
|
@@ -20,6 +20,11 @@ import type {
|
|
|
20
20
|
RpcHostToolUpdate,
|
|
21
21
|
RpcResponse,
|
|
22
22
|
RpcSessionState,
|
|
23
|
+
RpcUnattendedAccepted,
|
|
24
|
+
RpcUnattendedDeclaration,
|
|
25
|
+
RpcWorkflowGate,
|
|
26
|
+
RpcWorkflowGateResolution,
|
|
27
|
+
RpcWorkflowGateResponse,
|
|
23
28
|
} from "./rpc-types";
|
|
24
29
|
|
|
25
30
|
/** Distributive Omit that works with union types */
|
|
@@ -97,7 +102,7 @@ function isRpcResponse(value: unknown): value is RpcResponse {
|
|
|
97
102
|
if (typeof value.success !== "boolean") return false;
|
|
98
103
|
if (value.id !== undefined && typeof value.id !== "string") return false;
|
|
99
104
|
if (value.success === false) {
|
|
100
|
-
return typeof value.error === "string";
|
|
105
|
+
return typeof value.error === "string" || isRecord(value.error);
|
|
101
106
|
}
|
|
102
107
|
return true;
|
|
103
108
|
}
|
|
@@ -130,6 +135,21 @@ function isRpcExtensionUiRequest(value: unknown): value is RpcExtensionUIRequest
|
|
|
130
135
|
return value.type === "extension_ui_request" && typeof value.id === "string" && typeof value.method === "string";
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
function isRpcWorkflowGate(value: unknown): value is RpcWorkflowGate {
|
|
139
|
+
if (!isRecord(value)) return false;
|
|
140
|
+
return (
|
|
141
|
+
value.type === "workflow_gate" &&
|
|
142
|
+
typeof value.gate_id === "string" &&
|
|
143
|
+
typeof value.stage === "string" &&
|
|
144
|
+
typeof value.kind === "string" &&
|
|
145
|
+
isRecord(value.schema) &&
|
|
146
|
+
typeof value.schema_hash === "string" &&
|
|
147
|
+
isRecord(value.context) &&
|
|
148
|
+
typeof value.created_at === "string" &&
|
|
149
|
+
value.required === true
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
|
|
134
154
|
if (typeof result === "string") {
|
|
135
155
|
return {
|
|
@@ -152,6 +172,7 @@ export class RpcClient {
|
|
|
152
172
|
#pendingHostToolCalls = new Map<string, { controller: AbortController }>();
|
|
153
173
|
#requestId = 0;
|
|
154
174
|
#extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
|
|
175
|
+
#workflowGateListeners: Set<(gate: RpcWorkflowGate) => void> = new Set();
|
|
155
176
|
#abortController = new AbortController();
|
|
156
177
|
|
|
157
178
|
constructor(private options: RpcClientOptions = {}) {
|
|
@@ -299,6 +320,49 @@ export class RpcClient {
|
|
|
299
320
|
};
|
|
300
321
|
}
|
|
301
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Subscribe to workflow lifecycle gates emitted by RPC mode.
|
|
325
|
+
*/
|
|
326
|
+
onWorkflowGate(listener: (gate: RpcWorkflowGate) => void): () => void {
|
|
327
|
+
this.#workflowGateListeners.add(listener);
|
|
328
|
+
return () => {
|
|
329
|
+
this.#workflowGateListeners.delete(listener);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Answer a workflow lifecycle gate and wait for the server resolution envelope.
|
|
335
|
+
*/
|
|
336
|
+
async respondGate(gateId: string, answer: unknown, idempotencyKey?: string): Promise<RpcWorkflowGateResolution> {
|
|
337
|
+
const response = await this.#send({
|
|
338
|
+
type: "workflow_gate_response",
|
|
339
|
+
gate_id: gateId,
|
|
340
|
+
answer,
|
|
341
|
+
idempotency_key: idempotencyKey,
|
|
342
|
+
});
|
|
343
|
+
return this.#getData(response);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Subscribe to extension UI requests emitted by the server (e.g. select /
|
|
348
|
+
* input / editor / confirm). Returns an unsubscribe function.
|
|
349
|
+
*/
|
|
350
|
+
onExtensionUiRequest(listener: (req: RpcExtensionUIRequest) => void): () => void {
|
|
351
|
+
this.#extensionUiListeners.add(listener);
|
|
352
|
+
return () => {
|
|
353
|
+
this.#extensionUiListeners.delete(listener);
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Enter unattended mode by declaring budget + scopes + action allowlist.
|
|
359
|
+
* Returns the accepted declaration, or rejects (fail-closed) on refusal.
|
|
360
|
+
*/
|
|
361
|
+
async negotiateUnattended(declaration: RpcUnattendedDeclaration): Promise<RpcUnattendedAccepted> {
|
|
362
|
+
const response = await this.#send({ type: "negotiate_unattended", declaration });
|
|
363
|
+
return this.#getData(response);
|
|
364
|
+
}
|
|
365
|
+
|
|
302
366
|
/**
|
|
303
367
|
* Get collected stderr output (useful for debugging).
|
|
304
368
|
*/
|
|
@@ -687,6 +751,13 @@ export class RpcClient {
|
|
|
687
751
|
return;
|
|
688
752
|
}
|
|
689
753
|
|
|
754
|
+
if (isRpcWorkflowGate(data)) {
|
|
755
|
+
for (const listener of this.#workflowGateListeners) {
|
|
756
|
+
listener(data);
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
690
761
|
if (isRpcHostToolCancelRequest(data)) {
|
|
691
762
|
this.#pendingHostToolCalls.get(data.targetId)?.controller.abort();
|
|
692
763
|
return;
|
|
@@ -798,7 +869,10 @@ export class RpcClient {
|
|
|
798
869
|
}
|
|
799
870
|
}
|
|
800
871
|
|
|
801
|
-
#writeFrame(
|
|
872
|
+
#writeFrame(
|
|
873
|
+
frame: RpcCommand | RpcWorkflowGateResponse | RpcHostToolResult | RpcHostToolUpdate,
|
|
874
|
+
onError?: (error: Error) => void,
|
|
875
|
+
): void {
|
|
802
876
|
if (!this.#process?.stdin) {
|
|
803
877
|
throw new Error("Client not started");
|
|
804
878
|
}
|
|
@@ -815,7 +889,9 @@ export class RpcClient {
|
|
|
815
889
|
#getData<T>(response: RpcResponse): T {
|
|
816
890
|
if (!response.success) {
|
|
817
891
|
const errorResponse = response as Extract<RpcResponse, { success: false }>;
|
|
818
|
-
throw new Error(
|
|
892
|
+
throw new Error(
|
|
893
|
+
typeof errorResponse.error === "string" ? errorResponse.error : JSON.stringify(errorResponse.error),
|
|
894
|
+
);
|
|
819
895
|
}
|
|
820
896
|
// Type assertion: we trust response.data matches T based on the command sent.
|
|
821
897
|
// This is safe because each public method specifies the correct T for its command.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
+
import * as path from "node:path";
|
|
13
14
|
import { $env, readJsonl, Snowflake } from "@gajae-code/utils";
|
|
14
15
|
import type {
|
|
15
16
|
ExtensionUIContext,
|
|
@@ -21,6 +22,9 @@ import type { AgentSession } from "../../session/agent-session";
|
|
|
21
22
|
import { initializeExtensions } from "../runtime-init";
|
|
22
23
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
23
24
|
import { rpcError as error } from "../shared/agent-wire/responses";
|
|
25
|
+
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
26
|
+
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
27
|
+
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
24
28
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
25
29
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
26
30
|
import type {
|
|
@@ -72,6 +76,15 @@ function shouldEmitRpcTitles(): boolean {
|
|
|
72
76
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
|
|
80
|
+
if (event.includes("denied")) return "denied";
|
|
81
|
+
if (event.includes("exceeded")) return "exceeded";
|
|
82
|
+
if (event.includes("abort")) return "aborted";
|
|
83
|
+
if (event.includes("rejected") || event.includes("conflict")) return "rejected";
|
|
84
|
+
if (event.includes("accepted") || event.includes("negotiated") || event.includes("emitted")) return "accepted";
|
|
85
|
+
return "info";
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export function requestRpcEditor(
|
|
76
89
|
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
77
90
|
output: RpcOutput,
|
|
@@ -159,6 +172,59 @@ export async function runRpcMode(
|
|
|
159
172
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
160
173
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
161
174
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
175
|
+
const auditLog = new UnattendedAuditLog(defaultAuditPath(session.sessionId, session.sessionManager.getCwd()), {
|
|
176
|
+
redactAnswers: true,
|
|
177
|
+
});
|
|
178
|
+
const recordAudit = (event: { event: string; [key: string]: unknown }) => {
|
|
179
|
+
const payload =
|
|
180
|
+
typeof event.payload === "object" && event.payload !== null
|
|
181
|
+
? (event.payload as Record<string, unknown>)
|
|
182
|
+
: undefined;
|
|
183
|
+
const gateId =
|
|
184
|
+
typeof event.gate_id === "string"
|
|
185
|
+
? event.gate_id
|
|
186
|
+
: typeof payload?.gate_id === "string"
|
|
187
|
+
? payload.gate_id
|
|
188
|
+
: undefined;
|
|
189
|
+
auditLog.record({
|
|
190
|
+
run_id: session.sessionId,
|
|
191
|
+
session_id: session.sessionId,
|
|
192
|
+
actor: typeof event.actor === "string" ? event.actor : undefined,
|
|
193
|
+
event: event.event,
|
|
194
|
+
outcome: auditOutcomeFor(event.event),
|
|
195
|
+
dedupe_key: `${event.event}:${gateId ?? "run"}:${JSON.stringify(payload ?? event)}`,
|
|
196
|
+
gate_id: gateId,
|
|
197
|
+
stage: typeof event.stage === "string" ? (event.stage as never) : undefined,
|
|
198
|
+
kind: typeof event.kind === "string" ? (event.kind as never) : undefined,
|
|
199
|
+
scope: typeof payload?.scope === "string" ? payload.scope : undefined,
|
|
200
|
+
action: typeof payload?.action === "string" ? payload.action : undefined,
|
|
201
|
+
budget: event.event === "budget_exceeded" ? (payload as never) : undefined,
|
|
202
|
+
answer_hash: typeof event.answer_hash === "string" ? event.answer_hash : undefined,
|
|
203
|
+
error: payload && event.event.endsWith("denied") ? payload : undefined,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
// Unattended control plane (#318/#319/#323/G011): routes negotiate_unattended +
|
|
207
|
+
// workflow_gate_response and lets skill runtimes emit gates over RPC.
|
|
208
|
+
const gateStore = new FileGateStore(
|
|
209
|
+
path.join(session.sessionManager.getCwd(), ".gjc", "state", "workflow-gates", `${session.sessionId}.json`),
|
|
210
|
+
);
|
|
211
|
+
const unattendedControlPlane = new UnattendedSessionControlPlane({
|
|
212
|
+
runId: session.sessionId,
|
|
213
|
+
sessionId: session.sessionId,
|
|
214
|
+
emitFrame: gate => output(gate),
|
|
215
|
+
store: gateStore,
|
|
216
|
+
audit: recordAudit,
|
|
217
|
+
getUsageSnapshot: () => {
|
|
218
|
+
const stats = session.getSessionStats();
|
|
219
|
+
return { tokens: stats.tokens.total, costUsd: stats.cost };
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
unattendedControlPlane
|
|
223
|
+
.recover()
|
|
224
|
+
.catch(err =>
|
|
225
|
+
output(error(undefined, "workflow_gate_recover", err instanceof Error ? err.message : String(err))),
|
|
226
|
+
);
|
|
227
|
+
session.setWorkflowGateEmitter(unattendedControlPlane);
|
|
162
228
|
|
|
163
229
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
164
230
|
const shutdownState = { requested: false };
|
|
@@ -419,6 +485,7 @@ export async function runRpcMode(
|
|
|
419
485
|
hostToolRegistry: hostToolBridge,
|
|
420
486
|
hostUriRegistry: hostUriBridge,
|
|
421
487
|
createUiContext: () => new RpcExtensionUIContext(pendingExtensionRequests, output),
|
|
488
|
+
unattendedControlPlane,
|
|
422
489
|
});
|
|
423
490
|
|
|
424
491
|
/**
|