@gajae-code/coding-agent 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/config/model-registry.d.ts +17 -10
  3. package/dist/types/config/models-config-schema.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +5 -0
  5. package/dist/types/edit/diff.d.ts +16 -0
  6. package/dist/types/edit/modes/replace.d.ts +7 -0
  7. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  8. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  9. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  10. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  11. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  12. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  13. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  14. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  15. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  16. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  17. package/dist/types/extensibility/skills.d.ts +9 -1
  18. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
  20. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  21. package/dist/types/lsp/client.d.ts +1 -0
  22. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  23. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
  25. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  26. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  27. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  28. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  29. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  30. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  31. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  32. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  33. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  34. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  35. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  37. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  38. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  39. package/dist/types/modes/theme/theme.d.ts +2 -1
  40. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  41. package/dist/types/sdk.d.ts +7 -0
  42. package/dist/types/session/agent-session.d.ts +10 -0
  43. package/dist/types/session/blob-store.d.ts +17 -0
  44. package/dist/types/session/messages.d.ts +3 -0
  45. package/dist/types/session/session-storage.d.ts +6 -0
  46. package/dist/types/skill-state/active-state.d.ts +13 -0
  47. package/dist/types/thinking.d.ts +3 -2
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/package.json +9 -7
  50. package/src/cli.ts +14 -0
  51. package/src/commands/harness.ts +192 -7
  52. package/src/commands/ultragoal.ts +1 -21
  53. package/src/config/model-equivalence.ts +1 -1
  54. package/src/config/model-registry.ts +32 -5
  55. package/src/config/models-config-schema.ts +7 -2
  56. package/src/config/settings-schema.ts +4 -1
  57. package/src/discovery/claude-plugins.ts +25 -5
  58. package/src/edit/diff.ts +64 -1
  59. package/src/edit/modes/replace.ts +60 -2
  60. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  61. package/src/extensibility/gjc-plugins/index.ts +9 -0
  62. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  63. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  64. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  65. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  66. package/src/extensibility/gjc-plugins/state.ts +29 -0
  67. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  68. package/src/extensibility/gjc-plugins/types.ts +97 -0
  69. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  70. package/src/extensibility/skills.ts +39 -7
  71. package/src/gjc-runtime/state-runtime.ts +93 -2
  72. package/src/gjc-runtime/state-writer.ts +17 -1
  73. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  74. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  75. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  76. package/src/harness-control-plane/storage.ts +144 -2
  77. package/src/hashline/hash.ts +23 -0
  78. package/src/hooks/skill-state.ts +2 -0
  79. package/src/internal-urls/docs-index.generated.ts +5 -5
  80. package/src/lsp/client.ts +7 -0
  81. package/src/modes/acp/acp-agent.ts +25 -2
  82. package/src/modes/bridge/bridge-mode.ts +124 -2
  83. package/src/modes/controllers/input-controller.ts +14 -2
  84. package/src/modes/prompt-action-autocomplete.ts +49 -10
  85. package/src/modes/rpc/rpc-client.ts +57 -3
  86. package/src/modes/rpc/rpc-mode.ts +67 -0
  87. package/src/modes/rpc/rpc-types.ts +224 -2
  88. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  89. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  90. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  91. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  92. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  93. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  94. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  95. package/src/modes/shared/agent-wire/responses.ts +2 -2
  96. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  97. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  98. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  99. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  101. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  102. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  103. package/src/modes/theme/theme.ts +6 -0
  104. package/src/runtime-mcp/client.ts +7 -4
  105. package/src/runtime-mcp/manager.ts +45 -13
  106. package/src/runtime-mcp/transports/http.ts +40 -14
  107. package/src/runtime-mcp/transports/stdio.ts +11 -10
  108. package/src/sdk.ts +47 -0
  109. package/src/session/agent-session.ts +211 -2
  110. package/src/session/blob-store.ts +84 -0
  111. package/src/session/messages.ts +3 -0
  112. package/src/session/session-manager.ts +390 -33
  113. package/src/session/session-storage.ts +26 -0
  114. package/src/setup/provider-onboarding.ts +2 -2
  115. package/src/skill-state/active-state.ts +89 -1
  116. package/src/task/discovery.ts +7 -1
  117. package/src/task/executor.ts +16 -2
  118. package/src/thinking.ts +8 -2
  119. package/src/tools/ask.ts +39 -9
  120. package/src/tools/index.ts +3 -0
  121. package/src/tools/skill.ts +15 -3
  122. 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 built = await buildSkillPromptMessage(invocation.skill, invocation.args);
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 built = await buildSkillPromptMessage(skill, args);
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 { BridgeFrameSequencer, toBridgeEventFrame } from "../shared/agent-wire/event-envelope";
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 built = await buildSkillPromptMessage(invocation.skill, invocation.args);
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}${invocation.args ? ` ${invocation.args}` : ""}`;
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
- type AutocompleteItem,
3
- type AutocompleteProvider,
4
- CombinedAutocompleteProvider,
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 mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions);
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 mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions);
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,9 @@ import type {
20
20
  RpcHostToolUpdate,
21
21
  RpcResponse,
22
22
  RpcSessionState,
23
+ RpcWorkflowGate,
24
+ RpcWorkflowGateResolution,
25
+ RpcWorkflowGateResponse,
23
26
  } from "./rpc-types";
24
27
 
25
28
  /** Distributive Omit that works with union types */
@@ -97,7 +100,7 @@ function isRpcResponse(value: unknown): value is RpcResponse {
97
100
  if (typeof value.success !== "boolean") return false;
98
101
  if (value.id !== undefined && typeof value.id !== "string") return false;
99
102
  if (value.success === false) {
100
- return typeof value.error === "string";
103
+ return typeof value.error === "string" || isRecord(value.error);
101
104
  }
102
105
  return true;
103
106
  }
@@ -130,6 +133,21 @@ function isRpcExtensionUiRequest(value: unknown): value is RpcExtensionUIRequest
130
133
  return value.type === "extension_ui_request" && typeof value.id === "string" && typeof value.method === "string";
131
134
  }
132
135
 
136
+ function isRpcWorkflowGate(value: unknown): value is RpcWorkflowGate {
137
+ if (!isRecord(value)) return false;
138
+ return (
139
+ value.type === "workflow_gate" &&
140
+ typeof value.gate_id === "string" &&
141
+ typeof value.stage === "string" &&
142
+ typeof value.kind === "string" &&
143
+ isRecord(value.schema) &&
144
+ typeof value.schema_hash === "string" &&
145
+ isRecord(value.context) &&
146
+ typeof value.created_at === "string" &&
147
+ value.required === true
148
+ );
149
+ }
150
+
133
151
  function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
134
152
  if (typeof result === "string") {
135
153
  return {
@@ -152,6 +170,7 @@ export class RpcClient {
152
170
  #pendingHostToolCalls = new Map<string, { controller: AbortController }>();
153
171
  #requestId = 0;
154
172
  #extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
173
+ #workflowGateListeners: Set<(gate: RpcWorkflowGate) => void> = new Set();
155
174
  #abortController = new AbortController();
156
175
 
157
176
  constructor(private options: RpcClientOptions = {}) {
@@ -299,6 +318,29 @@ export class RpcClient {
299
318
  };
300
319
  }
301
320
 
321
+ /**
322
+ * Subscribe to workflow lifecycle gates emitted by RPC mode.
323
+ */
324
+ onWorkflowGate(listener: (gate: RpcWorkflowGate) => void): () => void {
325
+ this.#workflowGateListeners.add(listener);
326
+ return () => {
327
+ this.#workflowGateListeners.delete(listener);
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Answer a workflow lifecycle gate and wait for the server resolution envelope.
333
+ */
334
+ async respondGate(gateId: string, answer: unknown, idempotencyKey?: string): Promise<RpcWorkflowGateResolution> {
335
+ const response = await this.#send({
336
+ type: "workflow_gate_response",
337
+ gate_id: gateId,
338
+ answer,
339
+ idempotency_key: idempotencyKey,
340
+ });
341
+ return this.#getData(response);
342
+ }
343
+
302
344
  /**
303
345
  * Get collected stderr output (useful for debugging).
304
346
  */
@@ -687,6 +729,13 @@ export class RpcClient {
687
729
  return;
688
730
  }
689
731
 
732
+ if (isRpcWorkflowGate(data)) {
733
+ for (const listener of this.#workflowGateListeners) {
734
+ listener(data);
735
+ }
736
+ return;
737
+ }
738
+
690
739
  if (isRpcHostToolCancelRequest(data)) {
691
740
  this.#pendingHostToolCalls.get(data.targetId)?.controller.abort();
692
741
  return;
@@ -798,7 +847,10 @@ export class RpcClient {
798
847
  }
799
848
  }
800
849
 
801
- #writeFrame(frame: RpcCommand | RpcHostToolResult | RpcHostToolUpdate, onError?: (error: Error) => void): void {
850
+ #writeFrame(
851
+ frame: RpcCommand | RpcWorkflowGateResponse | RpcHostToolResult | RpcHostToolUpdate,
852
+ onError?: (error: Error) => void,
853
+ ): void {
802
854
  if (!this.#process?.stdin) {
803
855
  throw new Error("Client not started");
804
856
  }
@@ -815,7 +867,9 @@ export class RpcClient {
815
867
  #getData<T>(response: RpcResponse): T {
816
868
  if (!response.success) {
817
869
  const errorResponse = response as Extract<RpcResponse, { success: false }>;
818
- throw new Error(errorResponse.error);
870
+ throw new Error(
871
+ typeof errorResponse.error === "string" ? errorResponse.error : JSON.stringify(errorResponse.error),
872
+ );
819
873
  }
820
874
  // Type assertion: we trust response.data matches T based on the command sent.
821
875
  // 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
  /**