@gajae-code/coding-agent 0.8.0 → 0.8.2

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 (116) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/completion-cli.d.ts +41 -0
  3. package/dist/types/cli/config-cli.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +7 -0
  5. package/dist/types/cli.d.ts +124 -1
  6. package/dist/types/commands/completion.d.ts +29 -0
  7. package/dist/types/commands/config.d.ts +3 -0
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +7 -6
  10. package/dist/types/config/keybindings.d.ts +2 -2
  11. package/dist/types/config/model-profile-activation.d.ts +6 -0
  12. package/dist/types/config/settings.d.ts +8 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  14. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  17. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  19. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  21. package/dist/types/main.d.ts +4 -1
  22. package/dist/types/modes/components/model-selector.d.ts +4 -0
  23. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  26. package/dist/types/modes/print-mode.d.ts +13 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  28. package/dist/types/notifications/config.d.ts +8 -1
  29. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  30. package/dist/types/notifications/index.d.ts +6 -3
  31. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  32. package/dist/types/notifications/telegram-daemon.d.ts +18 -3
  33. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  34. package/dist/types/runtime-api-key.d.ts +7 -0
  35. package/dist/types/session/agent-session.d.ts +10 -0
  36. package/dist/types/session/agent-storage.d.ts +1 -0
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/tools/computer.d.ts +2 -2
  39. package/dist/types/tools/skill.d.ts +1 -1
  40. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  41. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  42. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  43. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  44. package/package.json +7 -7
  45. package/src/cli/completion-cli.ts +234 -0
  46. package/src/cli/config-cli.ts +37 -9
  47. package/src/cli/fast-help.ts +1 -0
  48. package/src/cli/notify-cli.ts +91 -7
  49. package/src/cli.ts +3 -1
  50. package/src/commands/completion.ts +84 -0
  51. package/src/commands/config.ts +2 -0
  52. package/src/commands/deep-interview.ts +2 -1
  53. package/src/config/file-lock.ts +73 -34
  54. package/src/config/keybindings.ts +2 -2
  55. package/src/config/model-profile-activation.ts +65 -0
  56. package/src/config/model-registry.ts +14 -2
  57. package/src/config/settings.ts +22 -0
  58. package/src/coordinator-mcp/server.ts +67 -12
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
  61. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  62. package/src/extensibility/extensions/runner.ts +2 -0
  63. package/src/extensibility/extensions/types.ts +9 -0
  64. package/src/extensibility/skills.ts +30 -2
  65. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  66. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  67. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  68. package/src/gjc-runtime/deep-interview-state.ts +13 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +59 -11
  70. package/src/gjc-runtime/state-runtime.ts +12 -2
  71. package/src/gjc-runtime/team-runtime.ts +23 -0
  72. package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
  73. package/src/hooks/native-skill-hook.ts +29 -0
  74. package/src/internal-urls/docs-index.generated.ts +2 -2
  75. package/src/main.ts +52 -41
  76. package/src/modes/acp/acp-agent.ts +7 -6
  77. package/src/modes/components/model-selector.ts +54 -11
  78. package/src/modes/components/queued-message-selector.ts +92 -0
  79. package/src/modes/components/settings-selector.ts +4 -0
  80. package/src/modes/components/welcome.ts +160 -44
  81. package/src/modes/controllers/command-controller.ts +4 -4
  82. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  83. package/src/modes/controllers/input-controller.ts +186 -25
  84. package/src/modes/controllers/selector-controller.ts +122 -27
  85. package/src/modes/interactive-mode.ts +43 -16
  86. package/src/modes/print-mode.ts +43 -4
  87. package/src/modes/prompt-action-autocomplete.ts +24 -13
  88. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  89. package/src/modes/utils/ui-helpers.ts +2 -2
  90. package/src/notifications/config.ts +9 -1
  91. package/src/notifications/daemon-paths.ts +2 -0
  92. package/src/notifications/index.ts +39 -9
  93. package/src/notifications/lifecycle-commands.ts +37 -7
  94. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  95. package/src/notifications/telegram-daemon-control.ts +3 -1
  96. package/src/notifications/telegram-daemon.ts +231 -43
  97. package/src/notifications/telegram-reference.ts +0 -16
  98. package/src/prompts/system/system-prompt.md +3 -0
  99. package/src/prompts/tools/computer.md +2 -2
  100. package/src/runtime-api-key.ts +16 -0
  101. package/src/sdk.ts +20 -6
  102. package/src/session/agent-session.ts +225 -31
  103. package/src/session/agent-storage.ts +4 -0
  104. package/src/session/session-manager.ts +44 -2
  105. package/src/slash-commands/builtin-registry.ts +153 -71
  106. package/src/task/executor.ts +7 -4
  107. package/src/task/index.ts +9 -3
  108. package/src/task/output-manager.ts +16 -6
  109. package/src/task/receipt.ts +26 -7
  110. package/src/tools/computer.ts +246 -43
  111. package/src/tools/skill.ts +1 -1
  112. package/src/tools/telegram-send.ts +6 -0
  113. package/src/workflow/workflow-intent-diff.ts +29 -6
  114. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  115. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  116. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -28,7 +28,7 @@ import type { ImageContent, TextContent } from "@gajae-code/ai";
28
28
  import { NotificationServer } from "@gajae-code/natives";
29
29
  import { logger, postmortem } from "@gajae-code/utils";
30
30
  import { Settings } from "../config/settings";
31
- import type { ExtensionCommandContext, ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
31
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../extensibility/extensions";
32
32
  import { registerAskAnswerSource } from "../tools/ask-answer-registry";
33
33
  import { registerTelegramFileSink } from "./attachment-registry";
34
34
  import {
@@ -100,7 +100,7 @@ export interface SessionCloseFrame {
100
100
  chatId: string;
101
101
  token: string;
102
102
  target: SessionCloseTarget;
103
- /** Hard-kill even if a live pane is attached (GJC-managed only). */
103
+ /** Required force-only close flag; false/omitted is rejected by daemon policy. */
104
104
  force?: boolean;
105
105
  }
106
106
 
@@ -336,6 +336,8 @@ interface SessionRuntime {
336
336
  disposeAnswerSource: () => void;
337
337
  /** Deregisters this session's Telegram file sink. */
338
338
  disposeFileSink: () => void;
339
+ /** Deregisters this session's unattended workflow-gate listener. */
340
+ disposeGateListener: () => void;
339
341
  redact: boolean;
340
342
  verbosity: "lean" | "verbose";
341
343
  sessionTag: string;
@@ -358,6 +360,8 @@ interface ResolvedSettings {
358
360
  settingsAvailable: boolean;
359
361
  }
360
362
 
363
+ const TELEGRAM_FILE_REDACTION_ERROR = "Telegram file attachments are disabled while notifications redaction is on.";
364
+
361
365
  const defaultConfig: NotificationConfig = {
362
366
  enabled: false,
363
367
  botToken: undefined,
@@ -379,7 +383,9 @@ export function notificationsEnabled(): boolean {
379
383
  return process.env.GJC_NOTIFICATIONS === "1" || Boolean(process.env.GJC_NOTIFICATIONS_TOKEN);
380
384
  }
381
385
 
382
- function resolveSettings(): ResolvedSettings {
386
+ function resolveSettings(settingsOverride?: Settings): ResolvedSettings {
387
+ if (settingsOverride)
388
+ return { settings: settingsOverride, cfg: getNotificationConfig(settingsOverride), settingsAvailable: true };
383
389
  try {
384
390
  const settings = Settings.instance;
385
391
  return { settings, cfg: getNotificationConfig(settings), settingsAvailable: true };
@@ -491,7 +497,7 @@ function sessionIdFromFile(file: string | undefined): string | undefined {
491
497
  return underscore >= 0 ? base.slice(underscore + 1) : undefined;
492
498
  }
493
499
 
494
- export const createNotificationsExtension: ExtensionFactory = api => {
500
+ export function createNotificationsExtension(api: ExtensionAPI, options: { settings?: Settings } = {}): void {
495
501
  const runtimes = new Map<string, SessionRuntime>();
496
502
  const disabledSessions = new Set<string>();
497
503
  const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
@@ -503,9 +509,16 @@ export const createNotificationsExtension: ExtensionFactory = api => {
503
509
  runtimes.delete(id);
504
510
  try {
505
511
  rt.cancelPostmortemCleanup();
512
+ } catch {}
513
+ try {
506
514
  rt.disposeAnswerSource();
515
+ } catch {}
516
+ try {
507
517
  rt.disposeFileSink();
508
518
  } catch {}
519
+ try {
520
+ rt.disposeGateListener();
521
+ } catch {}
509
522
  // Resolve any still-pending interactive asks so the ask tool is not left hanging.
510
523
  for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
511
524
  rt.pendingInteractive.clear();
@@ -529,10 +542,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
529
542
  return isSessionNotificationsEnabled({ cfg, env: process.env, sessionDisabled: disabledSessions.has(id) });
530
543
  }
531
544
 
545
+ function isNotificationEligibleContext(ctx: ExtensionContext): boolean {
546
+ return ctx.sessionMetadata?.kind !== "sub";
547
+ }
548
+
532
549
  async function startSession(ctx: ExtensionContext): Promise<"started" | "already" | "disabled" | "failed"> {
533
550
  const id = sessionId(ctx);
534
- const { settings, cfg, settingsAvailable } = resolveSettings();
535
- if (!isEnabledForSession(id, cfg)) return "disabled";
551
+ const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
552
+ if (!isNotificationEligibleContext(ctx) || !isEnabledForSession(id, cfg)) return "disabled";
536
553
  if (runtimes.has(id)) return "already";
537
554
 
538
555
  const stateRoot = path.join(ctx.cwd, ".gjc", "state");
@@ -660,6 +677,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
660
677
  tag,
661
678
  );
662
679
  const disposeFileSink = registerTelegramFileSink(id, async file => {
680
+ if (runtime?.redact ?? redact) {
681
+ return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
682
+ }
683
+
663
684
  try {
664
685
  const data = await fs.promises.readFile(file.path);
665
686
  server.pushFrame(
@@ -683,6 +704,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
683
704
  pendingInteractive,
684
705
  disposeAnswerSource,
685
706
  disposeFileSink,
707
+ disposeGateListener: () => {},
686
708
  cancelPostmortemCleanup: () => {},
687
709
  redact,
688
710
  verbosity,
@@ -726,7 +748,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
726
748
 
727
749
  // Unattended: a real ask emits a workflow gate; register it repliable by gate_id.
728
750
  if (unattended && gate?.onGateEmitted) {
729
- gate.onGateEmitted(g => {
751
+ runtime.disposeGateListener = gate.onGateEmitted(g => {
730
752
  const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
731
753
  gateOptions.set(g.gate_id, options);
732
754
  const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
@@ -761,7 +783,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
761
783
  async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
762
784
  const id = sessionId(ctx);
763
785
  const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
764
- const resolved = resolveSettings();
786
+ const resolved = resolveSettings(options.settings);
765
787
  const enabledWithoutLocalOff = isSessionNotificationsEnabled({
766
788
  cfg: resolved.cfg,
767
789
  env: process.env,
@@ -781,6 +803,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
781
803
  }
782
804
 
783
805
  if (command === "on") {
806
+ if (!isNotificationEligibleContext(ctx)) {
807
+ ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
808
+ return;
809
+ }
784
810
  if (process.env.GJC_NOTIFICATIONS === "0") {
785
811
  ctx.ui.notify(
786
812
  "Notifications remain disabled: GJC_NOTIFICATIONS=0 is an authoritative opt-out.",
@@ -878,6 +904,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
878
904
  rt.sessionTag,
879
905
  );
880
906
  rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
907
+ if (rt.redact) {
908
+ return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
909
+ }
910
+
881
911
  try {
882
912
  const data = await fs.promises.readFile(file.path);
883
913
  rt.server.pushFrame(
@@ -1099,4 +1129,4 @@ export const createNotificationsExtension: ExtensionFactory = api => {
1099
1129
  api.on("session_shutdown", async (_event, ctx) => {
1100
1130
  await stopSession(sessionId(ctx));
1101
1131
  });
1102
- };
1132
+ }
@@ -15,6 +15,19 @@ import * as os from "node:os";
15
15
  import type { SessionCloseTarget, SessionCreateTarget, SessionLifecycleResponse, SessionResumeTarget } from "./index";
16
16
 
17
17
  export type LifecycleCommandVerb = "session_create" | "session_close" | "session_resume";
18
+ function normalizeLifecycleCommandToken(
19
+ token: string,
20
+ ctx: { chatType?: string; botUsername?: string } = {},
21
+ ): string | undefined {
22
+ const at = token.indexOf("@");
23
+ const command = at === -1 ? token : token.slice(0, at);
24
+ if (!/^\/session_(create|close|resume|recent)\b/.test(command)) return undefined;
25
+ if (at === -1) return ctx.chatType === undefined || ctx.chatType === "private" ? command : undefined;
26
+
27
+ const botUsername = ctx.botUsername;
28
+ if (!botUsername || token.slice(at + 1).toLowerCase() !== botUsername.toLowerCase()) return undefined;
29
+ return command;
30
+ }
18
31
 
19
32
  /** A parsed, validated lifecycle command (transport identity added by caller). */
20
33
  export type ParsedLifecycleCommand =
@@ -36,10 +49,24 @@ const USAGE = [
36
49
  "/session_recent [create|resume]",
37
50
  ].join("\n");
38
51
 
39
- /** True when the text begins a /session_* command (cheap pre-gate). */
40
- export function isLifecycleCommandText(text: string | undefined): boolean {
52
+ /** True when the text begins with any /session_* token, regardless of addressability. */
53
+ export function isLifecycleCommandLikeText(text: string | undefined): boolean {
54
+ if (!text) return false;
55
+ const [token] = text.trim().split(/\s+/, 1);
56
+ if (!token) return false;
57
+ const at = token.indexOf("@");
58
+ const command = at === -1 ? token : token.slice(0, at);
59
+ return /^\/session_(create|close|resume|recent)\b/.test(command);
60
+ }
61
+
62
+ /** True when the text begins an addressable /session_* command (cheap pre-gate). */
63
+ export function isLifecycleCommandText(
64
+ text: string | undefined,
65
+ ctx: { chatType?: string; botUsername?: string } = {},
66
+ ): boolean {
41
67
  if (!text) return false;
42
- return /^\/session_(create|close|resume|recent)\b/.test(text.trim());
68
+ const [rawCommand] = text.trim().split(/\s+/, 1);
69
+ return normalizeLifecycleCommandToken(rawCommand ?? "", ctx) !== undefined;
43
70
  }
44
71
 
45
72
  /**
@@ -50,9 +77,14 @@ export function isLifecycleCommandText(text: string | undefined): boolean {
50
77
  * The caller MUST have already enforced paired-chat authorization; this function
51
78
  * performs grammar + target validation only.
52
79
  */
53
- export function parseLifecycleCommand(text: string | undefined): ParsedLifecycleCommand {
54
- if (!isLifecycleCommandText(text)) return { kind: "none" };
80
+ export function parseLifecycleCommand(
81
+ text: string | undefined,
82
+ ctx: { chatType?: string; botUsername?: string } = {},
83
+ ): ParsedLifecycleCommand {
55
84
  const raw = (text ?? "").trim();
85
+ const [rawCommand, ...args] = raw.split(/\s+/);
86
+ const command = normalizeLifecycleCommandToken(rawCommand ?? "", ctx);
87
+ if (command === undefined) return { kind: "none" };
56
88
 
57
89
  // MVP: reject any initial-prompt separator outright (no prompt handling yet).
58
90
  if (/\s--(\s|$)/.test(raw)) {
@@ -63,8 +95,6 @@ export function parseLifecycleCommand(text: string | undefined): ParsedLifecycle
63
95
  };
64
96
  }
65
97
 
66
- const [command, ...args] = raw.split(/\s+/);
67
-
68
98
  if (command === "/session_recent") {
69
99
  const which = args[0];
70
100
  if (which === undefined || which === "create" || which === "resume") {
@@ -252,7 +252,17 @@ export async function handleLifecycleRequest(
252
252
  return { status: "error", reason: "rate_limited", message: "create rate limit exceeded" };
253
253
  }
254
254
 
255
- // 4. Preallocate ids + write in_progress (fsynced) BEFORE any spawn.
255
+ // 4. Close is intentionally force-only until a graceful close contract exists.
256
+ if (frame.type === "session_close" && frame.force !== true) {
257
+ await deps.audit({ ...baseAudit, event: "rejected", reason: "invalid_target" });
258
+ return {
259
+ status: "error",
260
+ reason: "invalid_target",
261
+ message: "session_close requires force=true; graceful close is not supported",
262
+ };
263
+ }
264
+
265
+ // 5. Preallocate ids + write in_progress (fsynced) BEFORE any spawn.
256
266
  const lifecycleRequestId = frame.type === "session_create" ? frame.lifecycleRequestId : deps.newLifecycleRequestId();
257
267
  const intendedSessionId =
258
268
  frame.type === "session_create" ? frame.intendedSessionId || deps.newSessionId() : deps.newSessionId();
@@ -344,15 +354,18 @@ export async function handleLifecycleRequest(
344
354
  await deps.audit({ ...baseAudit, event: "success", sessionId: resumed.sessionId });
345
355
  return { status: "ok", entry, mode: resumed.mode };
346
356
  } catch (err) {
347
- // A side effect may have occurred; do not auto-respawn. Mark terminal
348
- // uncertain so a retry reconciles instead of duplicating.
357
+ // A side effect may have occurred; do not repeat it automatically. Mark
358
+ // terminal-uncertain so a retry reconciles instead of duplicating work.
359
+ let reason: LifecycleErrorReason = "terminal_uncertain";
360
+ if (frame.type === "session_create") reason = "spawn_failed";
361
+ if (frame.type === "session_close") reason = "close_refused";
349
362
  Object.assign(entry, {
350
363
  state: "terminal_uncertain",
351
364
  updatedAt: deps.now(),
352
- reason: "spawn_failed",
365
+ reason,
353
366
  });
354
367
  await deps.store.write(doc);
355
- await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason: "spawn_failed" });
356
- return { status: "error", reason: "terminal_uncertain", message: `lifecycle effect failed: ${String(err)}` };
368
+ await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason });
369
+ return { status: "error", reason: "terminal_uncertain", message: `${frame.type} effect failed: ${String(err)}` };
357
370
  }
358
371
  }
@@ -269,6 +269,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
269
269
  { settings: this.settings, roots, tokenFingerprint: fp, chatId },
270
270
  this.spawnDeps(),
271
271
  );
272
+ warnings.push(...spawned.warnings);
272
273
  const after = await this.status();
273
274
  return this.result(
274
275
  action,
@@ -351,6 +352,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
351
352
  { settings: this.settings, roots, tokenFingerprint: fp, chatId },
352
353
  this.spawnDeps(),
353
354
  );
355
+ warnings.push(...spawned.warnings);
354
356
  const after = await this.status();
355
357
  if (spawned.result === "attached") {
356
358
  // A live owner already exists; attaching to it is a valid running end-state.
@@ -360,7 +362,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
360
362
  }
361
363
  return this.result(
362
364
  action,
363
- spawned.result !== "disabled",
365
+ spawned.result !== "disabled" && spawned.result !== "blocked",
364
366
  `reloaded telegram daemon (${spawned.result})`,
365
367
  before,
366
368
  after,