@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +39 -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 +19 -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/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
  58. package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
  59. package/src/discovery/claude-plugins.ts +25 -5
  60. package/src/edit/diff.ts +64 -1
  61. package/src/edit/modes/replace.ts +60 -2
  62. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  63. package/src/extensibility/gjc-plugins/index.ts +9 -0
  64. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  65. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  66. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  67. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  68. package/src/extensibility/gjc-plugins/state.ts +29 -0
  69. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  70. package/src/extensibility/gjc-plugins/types.ts +97 -0
  71. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  72. package/src/extensibility/skills.ts +39 -7
  73. package/src/gjc-runtime/state-runtime.ts +93 -2
  74. package/src/gjc-runtime/state-writer.ts +17 -1
  75. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  76. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  77. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  78. package/src/harness-control-plane/storage.ts +144 -2
  79. package/src/hashline/hash.ts +23 -0
  80. package/src/hooks/skill-state.ts +2 -0
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/lsp/client.ts +7 -0
  83. package/src/modes/acp/acp-agent.ts +25 -2
  84. package/src/modes/bridge/bridge-mode.ts +124 -2
  85. package/src/modes/controllers/input-controller.ts +14 -2
  86. package/src/modes/prompt-action-autocomplete.ts +49 -10
  87. package/src/modes/rpc/rpc-client.ts +79 -3
  88. package/src/modes/rpc/rpc-mode.ts +67 -0
  89. package/src/modes/rpc/rpc-types.ts +224 -2
  90. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  91. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  92. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  93. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  94. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  95. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  96. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  97. package/src/modes/shared/agent-wire/responses.ts +2 -2
  98. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  99. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  100. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  101. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  102. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  103. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  104. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  105. package/src/modes/theme/theme.ts +6 -0
  106. package/src/prompts/system/system-prompt.md +9 -0
  107. package/src/runtime-mcp/client.ts +7 -4
  108. package/src/runtime-mcp/manager.ts +45 -13
  109. package/src/runtime-mcp/transports/http.ts +40 -14
  110. package/src/runtime-mcp/transports/stdio.ts +11 -10
  111. package/src/sdk.ts +47 -0
  112. package/src/session/agent-session.ts +211 -2
  113. package/src/session/blob-store.ts +84 -0
  114. package/src/session/messages.ts +3 -0
  115. package/src/session/session-manager.ts +390 -33
  116. package/src/session/session-storage.ts +26 -0
  117. package/src/setup/provider-onboarding.ts +2 -2
  118. package/src/skill-state/active-state.ts +89 -1
  119. package/src/task/discovery.ts +7 -1
  120. package/src/task/executor.ts +16 -2
  121. package/src/thinking.ts +8 -2
  122. package/src/tools/ask.ts +39 -9
  123. package/src/tools/index.ts +3 -0
  124. package/src/tools/skill.ts +15 -3
  125. 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,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(frame: RpcCommand | RpcHostToolResult | RpcHostToolUpdate, onError?: (error: Error) => void): void {
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(errorResponse.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
  /**