@gajae-code/coding-agent 0.7.8 → 0.7.9

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 (60) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -0
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/model-profile-activation.d.ts +19 -2
  5. package/dist/types/config/model-profiles.d.ts +1 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +2 -2
  8. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -3
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
  10. package/dist/types/modes/components/hook-input.d.ts +1 -0
  11. package/dist/types/modes/components/model-selector.d.ts +9 -0
  12. package/dist/types/modes/components/status-line.d.ts +3 -0
  13. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  14. package/dist/types/modes/interactive-mode.d.ts +3 -1
  15. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
  16. package/dist/types/modes/types.d.ts +3 -1
  17. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  18. package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
  19. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  20. package/dist/types/tools/ask.d.ts +24 -0
  21. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  22. package/package.json +7 -7
  23. package/src/cli/notify-cli.ts +1 -1
  24. package/src/cli.ts +1 -0
  25. package/src/config/keybindings.ts +1 -1
  26. package/src/config/model-profile-activation.ts +92 -11
  27. package/src/config/model-profiles.ts +15 -2
  28. package/src/config/model-registry.ts +81 -5
  29. package/src/config/settings-schema.ts +3 -2
  30. package/src/coordinator-mcp/server.ts +4 -2
  31. package/src/deep-interview/render-middleware.ts +1 -0
  32. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
  33. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  34. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  35. package/src/gjc-runtime/launch-tmux.ts +303 -46
  36. package/src/gjc-runtime/team-runtime.ts +69 -4
  37. package/src/gjc-runtime/ultragoal-guard.ts +11 -5
  38. package/src/internal-urls/docs-index.generated.ts +6 -6
  39. package/src/modes/components/custom-model-preset-wizard.ts +2 -4
  40. package/src/modes/components/hook-editor.ts +7 -4
  41. package/src/modes/components/hook-input.ts +4 -0
  42. package/src/modes/components/model-selector.ts +84 -13
  43. package/src/modes/components/settings-selector.ts +36 -21
  44. package/src/modes/components/status-line/segments.ts +10 -2
  45. package/src/modes/components/status-line.ts +1 -0
  46. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  47. package/src/modes/controllers/selector-controller.ts +130 -7
  48. package/src/modes/interactive-mode.ts +7 -2
  49. package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
  50. package/src/modes/types.ts +6 -1
  51. package/src/notifications/daemon-paths.ts +22 -0
  52. package/src/notifications/telegram-daemon-cli.ts +13 -4
  53. package/src/notifications/telegram-daemon.ts +2 -21
  54. package/src/prompts/system/system-prompt.md +5 -0
  55. package/src/sdk.ts +10 -1
  56. package/src/session/agent-session.ts +10 -1
  57. package/src/tools/ask.ts +11 -1
  58. package/src/tools/eval.ts +2 -2
  59. package/src/tools/render-utils.ts +2 -0
  60. package/src/tools/ultragoal-ask-guard.ts +39 -5
@@ -11,7 +11,7 @@
11
11
  * the interactive select/editor UI) when an unattended controller + gate broker
12
12
  * are attached is wired with the transport in #321 and exercised by #323.
13
13
  */
14
- import type { RpcJsonSchema } from "../../rpc/rpc-types";
14
+ import type { RpcJsonSchema, RpcWorkflowGateKind, RpcWorkflowStage } from "../../rpc/rpc-types";
15
15
  import type { OpenGateInput } from "./workflow-gate-broker";
16
16
 
17
17
  /** "Other (type your own)" sentinel, mirroring the interactive ask tool. */
@@ -26,6 +26,11 @@ export interface AskGateDeepInterviewState {
26
26
  ambiguity: number;
27
27
  }
28
28
 
29
+ export interface AskGateWorkflowGateMeta {
30
+ stage: RpcWorkflowStage;
31
+ kind: RpcWorkflowGateKind;
32
+ }
33
+
29
34
  export interface AskGateQuestion {
30
35
  id: string;
31
36
  question: string;
@@ -37,6 +42,8 @@ export interface AskGateQuestion {
37
42
  * `stage_state`; when absent, the question text is regex-parsed as a fallback.
38
43
  */
39
44
  deepInterview?: AskGateDeepInterviewState;
45
+ /** Override the emitted workflow gate address for non-deep-interview ask prompts. */
46
+ workflowGate?: AskGateWorkflowGateMeta;
40
47
  }
41
48
 
42
49
  export interface AskGateResult {
@@ -157,13 +164,13 @@ function structuredDeepInterviewState(meta: AskGateDeepInterviewState): Record<s
157
164
  return state;
158
165
  }
159
166
 
160
- /** Build the `workflow_gate` open-input for one deep-interview question. */
167
+ /** Build the `workflow_gate` open-input for one ask question. */
161
168
  export function questionToGate(question: AskGateQuestion): OpenGateInput {
162
169
  const labels = question.options.map(o => o.label);
163
170
  const schema = questionAnswerSchema(question, labels);
164
171
  return {
165
- stage: "deep-interview",
166
- kind: "question",
172
+ stage: question.workflowGate?.stage ?? "deep-interview",
173
+ kind: question.workflowGate?.kind ?? "question",
167
174
  schema,
168
175
  options: question.options.map((o, i) => ({
169
176
  value: o.label,
@@ -290,7 +290,12 @@ export interface InteractiveModeContext {
290
290
  dialogOptions?: ExtensionUIDialogOptions,
291
291
  ): Promise<string | undefined>;
292
292
  hideHookSelector(): void;
293
- showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
293
+ showHookInput(
294
+ title: string,
295
+ placeholder?: string,
296
+ dialogOptions?: ExtensionUIDialogOptions,
297
+ inputOptions?: { readonly initialValue?: string },
298
+ ): Promise<string | undefined>;
294
299
  hideHookInput(): void;
295
300
  showHookEditor(
296
301
  title: string,
@@ -0,0 +1,22 @@
1
+ import * as path from "node:path";
2
+
3
+ export interface DaemonPaths {
4
+ dir: string;
5
+ lock: string;
6
+ state: string;
7
+ roots: string;
8
+ steal: string;
9
+ aliases: string;
10
+ }
11
+
12
+ export function daemonPaths(agentDir: string): DaemonPaths {
13
+ const dir = path.join(agentDir, "notifications");
14
+ return {
15
+ dir,
16
+ lock: path.join(dir, "telegram-daemon.lock"),
17
+ state: path.join(dir, "telegram-daemon.state.json"),
18
+ roots: path.join(dir, "telegram-daemon.roots.json"),
19
+ steal: path.join(dir, "telegram-daemon.steal"),
20
+ aliases: path.join(dir, "telegram-callback-aliases.json"),
21
+ };
22
+ }
@@ -3,12 +3,19 @@ import * as path from "node:path";
3
3
  import { YAML } from "bun";
4
4
  import type { Settings } from "../config/settings";
5
5
  import { getNotificationConfig, isGloballyConfigured } from "./config";
6
- import { daemonPaths, TelegramNotificationDaemon } from "./telegram-daemon";
7
- import { clearTelegramControlRequest, readTelegramControlRequest } from "./telegram-daemon-control";
6
+ import { daemonPaths } from "./daemon-paths";
7
+ import type { TelegramDaemonOptions } from "./telegram-daemon";
8
+
9
+ type TelegramDaemonRunner = {
10
+ run(): Promise<void>;
11
+ requestStop(reason?: "reload" | "signal" | "stop"): void;
12
+ };
13
+
14
+ type TelegramDaemonConstructor = new (opts: TelegramDaemonOptions) => TelegramDaemonRunner;
8
15
 
9
16
  export interface RunDaemonInternalDeps {
10
17
  SettingsImpl?: { init: (options?: { agentDir?: string }) => Promise<Pick<Settings, "get" | "getAgentDir">> };
11
- DaemonImpl?: typeof TelegramNotificationDaemon;
18
+ DaemonImpl?: TelegramDaemonConstructor;
12
19
  processPid?: number;
13
20
  pidAlive?: (pid: number) => boolean;
14
21
  }
@@ -140,7 +147,9 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
140
147
  const settings = await resolveDaemonSettings(resolvedAgentDir, deps);
141
148
  const cfg = getNotificationConfig(settings as Settings);
142
149
  if (!isGloballyConfigured(cfg) || !cfg.botToken || !cfg.chatId) return;
143
- const Daemon = deps.DaemonImpl ?? TelegramNotificationDaemon;
150
+ const { clearTelegramControlRequest, readTelegramControlRequest } = await import("./telegram-daemon-control");
151
+ const Daemon: TelegramDaemonConstructor =
152
+ deps.DaemonImpl ?? (await import("./telegram-daemon")).TelegramNotificationDaemon;
144
153
  const daemon = new Daemon({
145
154
  settings: settings as Settings,
146
155
  ownerId,
@@ -10,6 +10,7 @@ import type { DaemonRuntimeInfo } from "../daemon/control-types";
10
10
  import { resolveGjcRuntimeSpawnInfo } from "../daemon/runtime";
11
11
  import { getNotificationConfig, isGloballyConfigured, tokenFingerprint } from "./config";
12
12
  import { parseInThreadConfigCommand } from "./config-commands";
13
+ import { daemonPaths } from "./daemon-paths";
13
14
  import { buildCompactChoiceGrid, TELEGRAM_PARSE_MODE } from "./html-format";
14
15
  import type {
15
16
  SessionCloseTarget,
@@ -62,16 +63,6 @@ export interface DaemonState {
62
63
  version: 1;
63
64
  stoppedAt?: number;
64
65
  }
65
-
66
- export interface DaemonPaths {
67
- dir: string;
68
- lock: string;
69
- state: string;
70
- roots: string;
71
- steal: string;
72
- aliases: string;
73
- }
74
-
75
66
  export interface TelegramDaemonFs {
76
67
  mkdir(path: string, opts?: fs.MakeDirectoryOptions): Promise<void>;
77
68
  readFile(path: string, encoding: BufferEncoding): Promise<string>;
@@ -184,17 +175,7 @@ async function fetchWithRetry(
184
175
  throw lastErr;
185
176
  }
186
177
 
187
- export function daemonPaths(agentDir: string): DaemonPaths {
188
- const dir = path.join(agentDir, "notifications");
189
- return {
190
- dir,
191
- lock: path.join(dir, "telegram-daemon.lock"),
192
- state: path.join(dir, "telegram-daemon.state.json"),
193
- roots: path.join(dir, "telegram-daemon.roots.json"),
194
- steal: path.join(dir, "telegram-daemon.steal"),
195
- aliases: path.join(dir, "telegram-callback-aliases.json"),
196
- };
197
- }
178
+ export { type DaemonPaths, daemonPaths } from "./daemon-paths";
198
179
 
199
180
  /**
200
181
  * Attach session-lifecycle control (create/close/resume) to the running daemon.
@@ -82,6 +82,11 @@ Use for read-only plan critique. It approves only when execution can proceed wit
82
82
  - Do not load or inject user-home Anthropic model or provider instructions (`~/.anthropic-model`, `~/.openai-code`) into the model context.
83
83
  - Public commands, paths, examples, and workflow names must use `gjc` and `.gjc`.
84
84
  </runtime-state>
85
+ <self-awareness>
86
+ - When the user asks about GJC usage, how to use a GJC feature/command/workflow, or about the gajae-code system itself, do not answer from memory alone. First clone the canonical source repository into `/tmp` (e.g. `git clone --depth 1 https://github.com/Yeachan-Heo/gajae-code /tmp/gajae-code-<unique>`), then read and analyze the actual source there to ground your answer.
87
+ - Reuse an existing fresh clone under `/tmp` instead of re-cloning when one is already present in the session.
88
+ - Base usage and system answers on what the cloned source actually says; cite concrete files/paths from the clone rather than guessing.
89
+ </self-awareness>
85
90
  </gjc-runtime>
86
91
 
87
92
  <communication>
package/src/sdk.ts CHANGED
@@ -2007,7 +2007,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2007
2007
  .map(name => toolRegistry.get(name))
2008
2008
  .filter((tool): tool is AgentTool => tool !== undefined)
2009
2009
  // AgentSession tool wrapping is not installed until after Agent construction.
2010
- .map(tool => guardToolForUltragoalAsk(tool, () => sessionManager.getCwd()));
2010
+ .map(tool =>
2011
+ guardToolForUltragoalAsk(
2012
+ tool,
2013
+ () => sessionManager.getCwd(),
2014
+ () => ({
2015
+ activeSkillState: session?.getActiveSkillState(),
2016
+ sessionId: sessionManager.getSessionId?.() ?? null,
2017
+ }),
2018
+ ),
2019
+ );
2011
2020
 
2012
2021
  const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "off";
2013
2022
  const preferOpenAICodexWebsockets =
@@ -3920,7 +3920,16 @@ export class AgentSession {
3920
3920
 
3921
3921
  #prepareToolForExecution<T extends AgentTool>(tool: T): T {
3922
3922
  return this.#wrapToolForDeepInterviewMutationGuard(
3923
- this.#wrapToolForAcpPermission(guardToolForUltragoalAsk(tool, () => this.sessionManager.getCwd())),
3923
+ this.#wrapToolForAcpPermission(
3924
+ guardToolForUltragoalAsk(
3925
+ tool,
3926
+ () => this.sessionManager.getCwd(),
3927
+ () => ({
3928
+ activeSkillState: this.getActiveSkillState(),
3929
+ sessionId: this.sessionManager.getSessionId(),
3930
+ }),
3931
+ ),
3932
+ ),
3924
3933
  );
3925
3934
  }
3926
3935
 
package/src/tools/ask.ts CHANGED
@@ -62,6 +62,11 @@ const DeepInterviewMeta = z.object({
62
62
  ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
63
63
  });
64
64
 
65
+ const WorkflowGateMeta = z.object({
66
+ stage: z.enum(["deep-interview", "ralplan", "ultragoal"]).describe("workflow gate stage"),
67
+ kind: z.enum(["question", "approval", "execution"]).describe("workflow gate kind"),
68
+ });
69
+
65
70
  const QuestionItem = z.object({
66
71
  id: z.string().describe("question id"),
67
72
  question: z.string().describe("question text"),
@@ -69,6 +74,7 @@ const QuestionItem = z.object({
69
74
  multi: z.boolean().describe("allow multiple selections").optional(),
70
75
  recommended: z.number().describe("recommended option index").optional(),
71
76
  deepInterview: DeepInterviewMeta.describe("optional deep-interview round metadata").optional(),
77
+ workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
72
78
  });
73
79
 
74
80
  export const askSchema = z.object({
@@ -551,7 +557,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
551
557
  _onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
552
558
  context?: AgentToolContext,
553
559
  ): Promise<AgentToolResult<AskToolDetails>> {
554
- await assertUltragoalAskAllowed(this.session.cwd);
560
+ await assertUltragoalAskAllowed(this.session.cwd, {
561
+ activeSkillState: this.session.getActiveSkillState?.(),
562
+ sessionId: this.session.getSessionId?.() ?? null,
563
+ });
555
564
  const gateEmitter = this.session.getWorkflowGateEmitter?.();
556
565
  const canUseWorkflowGate = gateEmitter?.isUnattended() === true;
557
566
 
@@ -665,6 +674,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
665
674
  multi: q.multi,
666
675
  recommended: q.recommended,
667
676
  deepInterview: q.deepInterview,
677
+ workflowGate: q.workflowGate,
668
678
  };
669
679
  const answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
670
680
  const decoded = gateAnswerToResult(gateQuestion, answer);
package/src/tools/eval.ts CHANGED
@@ -754,8 +754,8 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
754
754
  case "git_log":
755
755
  if (data.entries) {
756
756
  addItems(data.entries as unknown[], e => {
757
- const entry = e as { sha: string; subject: string };
758
- return `${entry.sha} ${truncateToWidth(entry.subject, 50)}`;
757
+ const entry = e as { sha?: string; subject?: string };
758
+ return `${String(entry.sha ?? "")} ${truncateToWidth(String(entry.subject ?? ""), 50)}`;
759
759
  });
760
760
  }
761
761
  break;
@@ -86,6 +86,7 @@ export const EXPAND_HINT = "(Ctrl+O for more)";
86
86
  * Get first N lines of text as preview, with each line truncated.
87
87
  */
88
88
  export function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis?: Ellipsis): string[] {
89
+ if (typeof text !== "string") return [];
89
90
  const lines = text.split("\n").filter(l => l.trim());
90
91
  return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
91
92
  }
@@ -568,6 +569,7 @@ export function truncateDiffByHunk(
568
569
  // =============================================================================
569
570
 
570
571
  export function shortenPath(filePath: string, homeDir?: string): string {
572
+ if (typeof filePath !== "string") return "";
571
573
  const home = homeDir ?? os.homedir();
572
574
  if (home && filePath.startsWith(home)) {
573
575
  return `~${filePath.slice(home.length)}`;
@@ -10,6 +10,29 @@ const ULTRAGOAL_ASK_GUARD = Symbol.for("gajae-code.ultragoalAskGuard");
10
10
 
11
11
  type GuardedTool = AgentTool & { [ULTRAGOAL_ASK_GUARD]?: true };
12
12
 
13
+ export interface UltragoalAskGuardContext {
14
+ activeSkillState?: { skill?: string; session_id?: string } | null;
15
+ sessionId?: string | null;
16
+ }
17
+
18
+ const UPSTREAM_PLANNING_ASK_SKILLS = new Set(["deep-interview", "ralplan"]);
19
+
20
+ function normalizedActiveSkill(context?: UltragoalAskGuardContext): string | undefined {
21
+ const skill = context?.activeSkillState?.skill?.trim();
22
+ return skill || undefined;
23
+ }
24
+
25
+ function sessionScopedAskGuardId(
26
+ context: UltragoalAskGuardContext,
27
+ activeSkill: string | undefined,
28
+ ): string | undefined {
29
+ if (activeSkill !== "ultragoal" && !UPSTREAM_PLANNING_ASK_SKILLS.has(activeSkill ?? "")) return undefined;
30
+ const activeSessionId = context.activeSkillState?.session_id?.trim();
31
+ if (activeSessionId) return activeSessionId;
32
+ const sessionId = context.sessionId?.trim();
33
+ return sessionId || undefined;
34
+ }
35
+
13
36
  export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string {
14
37
  return [
15
38
  diagnostic.message,
@@ -18,15 +41,26 @@ export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiag
18
41
  ].join("\n");
19
42
  }
20
43
 
21
- export async function assertUltragoalAskAllowed(cwd: string): Promise<void> {
22
- const diagnostic = await isUltragoalAskBlocked(cwd);
44
+ export async function assertUltragoalAskAllowed(cwd: string, context: UltragoalAskGuardContext = {}): Promise<void> {
45
+ const activeSkill = normalizedActiveSkill(context);
46
+ // Deep-interview and ralplan are upstream planning workflows whose core gates
47
+ // are `ask` calls. Scope their Ultragoal check to the current session so stale
48
+ // or ambiguous Ultragoal durable state from another session cannot hijack those
49
+ // prompts; same-session active Ultragoal state still falls through to the
50
+ // blocker/nudge checks below.
51
+ const sessionId = sessionScopedAskGuardId(context, activeSkill);
52
+ const diagnostic = await isUltragoalAskBlocked(cwd, { sessionId });
23
53
  if (!diagnostic.active) return;
24
- const nudge = await consumeUltragoalAskNudge(cwd);
54
+ const nudge = await consumeUltragoalAskNudge(cwd, sessionId);
25
55
  if (nudge.nudged) throw new ToolError(nudge.message);
26
56
  throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
27
57
  }
28
58
 
29
- export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string): T {
59
+ export function guardToolForUltragoalAsk<T extends AgentTool>(
60
+ tool: T,
61
+ getCwd: () => string,
62
+ getContext: () => UltragoalAskGuardContext = () => ({}),
63
+ ): T {
30
64
  if (tool.name !== "ask") return tool;
31
65
  const candidate = tool as GuardedTool;
32
66
  if (candidate[ULTRAGOAL_ASK_GUARD]) return tool;
@@ -35,7 +69,7 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: (
35
69
  if (prop === ULTRAGOAL_ASK_GUARD) return true;
36
70
  if (prop !== "execute") return Reflect.get(target, prop, receiver);
37
71
  return async (...args: unknown[]): Promise<unknown> => {
38
- await assertUltragoalAskAllowed(getCwd());
72
+ await assertUltragoalAskAllowed(getCwd(), getContext());
39
73
  return Reflect.apply(target.execute, target, args);
40
74
  };
41
75
  },