@gajae-code/coding-agent 0.1.2 → 0.2.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 (41) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
  3. package/dist/types/config/model-registry.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +4 -1
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
  6. package/dist/types/gjc-runtime/team-runtime.d.ts +40 -1
  7. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +15 -10
  8. package/dist/types/hooks/skill-state.d.ts +4 -1
  9. package/dist/types/modes/components/model-selector.d.ts +2 -4
  10. package/dist/types/modes/interactive-mode.d.ts +1 -0
  11. package/dist/types/sdk.d.ts +2 -4
  12. package/dist/types/session/agent-session.d.ts +3 -9
  13. package/dist/types/skill-state/active-state.d.ts +19 -0
  14. package/dist/types/skill-state/workflow-hud.d.ts +62 -0
  15. package/package.json +9 -9
  16. package/src/commands/deep-interview.ts +21 -2
  17. package/src/commands/gjc-runtime-bridge.ts +161 -15
  18. package/src/commands/ralplan.ts +21 -2
  19. package/src/commands/team.ts +54 -3
  20. package/src/commands/ultragoal.ts +21 -1
  21. package/src/config/model-registry.ts +4 -0
  22. package/src/config/model-resolver.ts +5 -1
  23. package/src/defaults/gjc/skills/deep-interview/SKILL.md +6 -6
  24. package/src/defaults/gjc/skills/ralplan/SKILL.md +5 -9
  25. package/src/defaults/gjc/skills/team/SKILL.md +5 -4
  26. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -8
  27. package/src/gjc-runtime/launch-tmux.ts +73 -2
  28. package/src/gjc-runtime/team-runtime.ts +365 -35
  29. package/src/gjc-runtime/ultragoal-guard.ts +43 -1
  30. package/src/gjc-runtime/ultragoal-runtime.ts +307 -187
  31. package/src/hooks/skill-state.ts +4 -1
  32. package/src/main.ts +1 -0
  33. package/src/modes/components/model-selector.ts +108 -8
  34. package/src/modes/components/skill-hud/render.ts +35 -8
  35. package/src/modes/interactive-mode.ts +34 -22
  36. package/src/prompts/system/system-prompt.md +5 -4
  37. package/src/sdk.ts +3 -1
  38. package/src/session/agent-session.ts +15 -3
  39. package/src/skill-state/active-state.ts +104 -4
  40. package/src/skill-state/workflow-hud.ts +160 -0
  41. package/src/tools/image-gen.ts +19 -10
@@ -7,6 +7,23 @@ export const SKILL_ACTIVE_STALE_MS = 24 * 60 * 60 * 1000;
7
7
  export const CANONICAL_GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "team"] as const;
8
8
 
9
9
  export type CanonicalGjcWorkflowSkill = (typeof CANONICAL_GJC_WORKFLOW_SKILLS)[number];
10
+ export type WorkflowHudSeverity = "info" | "warning" | "blocked" | "error" | "success";
11
+
12
+ export interface WorkflowHudChip {
13
+ label: string;
14
+ value?: string;
15
+ priority?: number;
16
+ severity?: WorkflowHudSeverity;
17
+ }
18
+
19
+ export interface WorkflowHudSummary {
20
+ version: 1;
21
+ summary?: string;
22
+ chips?: WorkflowHudChip[];
23
+ details?: WorkflowHudChip[];
24
+ severity?: WorkflowHudSeverity;
25
+ updated_at?: string;
26
+ }
10
27
 
11
28
  export interface SkillActiveEntry {
12
29
  skill: string;
@@ -17,6 +34,8 @@ export interface SkillActiveEntry {
17
34
  session_id?: string;
18
35
  thread_id?: string;
19
36
  turn_id?: string;
37
+ hud?: WorkflowHudSummary;
38
+ stale?: boolean;
20
39
  }
21
40
 
22
41
  export interface SkillActiveState {
@@ -50,12 +69,79 @@ export interface SyncSkillActiveStateOptions {
50
69
  turnId?: string;
51
70
  nowIso?: string;
52
71
  source?: string;
72
+ hud?: WorkflowHudSummary;
53
73
  }
54
74
 
75
+ const HUD_TEXT_LIMIT = 80;
76
+ const HUD_CHIP_LIMIT = 6;
77
+ const HUD_DETAIL_LIMIT = 12;
78
+ const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
79
+ const HUD_SEVERITIES = new Set<WorkflowHudSeverity>(["info", "warning", "blocked", "error", "success"]);
80
+
55
81
  function safeString(value: unknown): string {
56
82
  return typeof value === "string" ? value : "";
57
83
  }
58
84
 
85
+ function sanitizeHudString(value: unknown, limit = HUD_TEXT_LIMIT): string | undefined {
86
+ const normalized = safeString(value)
87
+ .replace(ANSI_PATTERN, "")
88
+ .replace(/[\r\n\t]+/g, " ")
89
+ .trim();
90
+ if (!normalized) return undefined;
91
+ return normalized.length > limit ? normalized.slice(0, limit) : normalized;
92
+ }
93
+
94
+ function normalizeSeverity(value: unknown): WorkflowHudSeverity | undefined {
95
+ return typeof value === "string" && HUD_SEVERITIES.has(value as WorkflowHudSeverity)
96
+ ? (value as WorkflowHudSeverity)
97
+ : undefined;
98
+ }
99
+
100
+ function normalizeHudChip(raw: unknown): WorkflowHudChip | null {
101
+ if (!raw || typeof raw !== "object") return null;
102
+ const record = raw as Record<string, unknown>;
103
+ const label = sanitizeHudString(record.label, 32);
104
+ if (!label) return null;
105
+ const value = sanitizeHudString(record.value, HUD_TEXT_LIMIT);
106
+ const priority =
107
+ typeof record.priority === "number" && Number.isFinite(record.priority) ? record.priority : undefined;
108
+ const severity = normalizeSeverity(record.severity);
109
+ return {
110
+ label,
111
+ ...(value ? { value } : {}),
112
+ ...(priority !== undefined ? { priority } : {}),
113
+ ...(severity ? { severity } : {}),
114
+ };
115
+ }
116
+
117
+ function normalizeHudChips(raw: unknown, limit: number): WorkflowHudChip[] | undefined {
118
+ if (!Array.isArray(raw)) return undefined;
119
+ const chips = raw
120
+ .map(normalizeHudChip)
121
+ .filter((chip): chip is WorkflowHudChip => chip !== null)
122
+ .slice(0, limit);
123
+ return chips.length > 0 ? chips : undefined;
124
+ }
125
+
126
+ export function normalizeWorkflowHudSummary(raw: unknown): WorkflowHudSummary | undefined {
127
+ if (!raw || typeof raw !== "object") return undefined;
128
+ const record = raw as Record<string, unknown>;
129
+ if (record.version !== 1) return undefined;
130
+ const summary = sanitizeHudString(record.summary);
131
+ const chips = normalizeHudChips(record.chips, HUD_CHIP_LIMIT);
132
+ const details = normalizeHudChips(record.details, HUD_DETAIL_LIMIT);
133
+ const severity = normalizeSeverity(record.severity);
134
+ const updatedAt = sanitizeHudString(record.updated_at, 40);
135
+ return {
136
+ version: 1,
137
+ ...(summary ? { summary } : {}),
138
+ ...(chips ? { chips } : {}),
139
+ ...(details ? { details } : {}),
140
+ ...(severity ? { severity } : {}),
141
+ ...(updatedAt ? { updated_at: updatedAt } : {}),
142
+ };
143
+ }
144
+
59
145
  function encodePathSegment(value: string): string {
60
146
  return encodeURIComponent(value).replaceAll(".", "%2E");
61
147
  }
@@ -70,16 +156,25 @@ function timestampMs(value: string | undefined): number | null {
70
156
  return Number.isFinite(ms) ? ms : null;
71
157
  }
72
158
 
159
+ function entryTimestampMs(entry: SkillActiveEntry): number | null {
160
+ return timestampMs(entry.hud?.updated_at) ?? timestampMs(entry.updated_at) ?? timestampMs(entry.activated_at);
161
+ }
162
+
73
163
  function isFreshEntry(entry: SkillActiveEntry, nowMs = Date.now()): boolean {
74
- const ms = timestampMs(entry.updated_at) ?? timestampMs(entry.activated_at);
164
+ const ms = entryTimestampMs(entry);
75
165
  return ms === null || nowMs - ms <= SKILL_ACTIVE_STALE_MS;
76
166
  }
77
167
 
168
+ function withDerivedStale(entry: SkillActiveEntry, nowMs = Date.now()): SkillActiveEntry {
169
+ return { ...entry, stale: !isFreshEntry(entry, nowMs) };
170
+ }
171
+
78
172
  function normalizeEntry(raw: unknown): SkillActiveEntry | null {
79
173
  if (!raw || typeof raw !== "object") return null;
80
174
  const record = raw as Record<string, unknown>;
81
175
  const skill = safeString(record.skill).trim();
82
176
  if (!skill) return null;
177
+ const hud = normalizeWorkflowHudSummary(record.hud);
83
178
  return {
84
179
  ...record,
85
180
  skill,
@@ -90,6 +185,8 @@ function normalizeEntry(raw: unknown): SkillActiveEntry | null {
90
185
  session_id: safeString(record.session_id).trim() || undefined,
91
186
  thread_id: safeString(record.thread_id).trim() || undefined,
92
187
  turn_id: safeString(record.turn_id).trim() || undefined,
188
+ ...(hud ? { hud } : {}),
189
+ stale: undefined,
93
190
  };
94
191
  }
95
192
 
@@ -185,11 +282,12 @@ function mergeVisibleEntries(
185
282
  rootState: SkillActiveState | null,
186
283
  sessionId?: string,
187
284
  ): SkillActiveEntry[] {
188
- const rootEntries = filterRootEntriesForSession(listActiveSkills(rootState), sessionId).filter(entry =>
189
- isFreshEntry(entry),
285
+ const nowMs = Date.now();
286
+ const rootEntries = filterRootEntriesForSession(listActiveSkills(rootState), sessionId).map(entry =>
287
+ withDerivedStale(entry, nowMs),
190
288
  );
191
289
  const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
192
- for (const entry of listActiveSkills(sessionState).filter(entry => isFreshEntry(entry))) {
290
+ for (const entry of listActiveSkills(sessionState).map(candidate => withDerivedStale(candidate, nowMs))) {
193
291
  merged.set(entryKey(entry), entry);
194
292
  }
195
293
  return [...merged.values()];
@@ -229,6 +327,7 @@ function upsertEntry(entries: SkillActiveEntry[], entry: SkillActiveEntry, activ
229
327
 
230
328
  export async function syncSkillActiveState(options: SyncSkillActiveStateOptions): Promise<void> {
231
329
  const nowIso = options.nowIso ?? new Date().toISOString();
330
+ const hud = normalizeWorkflowHudSummary(options.hud);
232
331
  const entry: SkillActiveEntry = {
233
332
  skill: options.skill,
234
333
  phase: options.phase,
@@ -238,6 +337,7 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
238
337
  session_id: options.sessionId,
239
338
  thread_id: options.threadId,
240
339
  turn_id: options.turnId,
340
+ ...(hud ? { hud } : {}),
241
341
  };
242
342
  const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, options.sessionId);
243
343
  const rootState = (await readStateFile(rootPath)) ?? { version: 1, active_skills: [] };
@@ -0,0 +1,160 @@
1
+ import type { WorkflowHudChip, WorkflowHudSummary } from "./active-state";
2
+
3
+ interface DeepInterviewHudState {
4
+ phase?: string;
5
+ ambiguity?: number;
6
+ threshold?: number;
7
+ roundCount?: number;
8
+ targetComponent?: string;
9
+ weakestDimension?: string;
10
+ specStatus?: string;
11
+ updatedAt?: string;
12
+ }
13
+
14
+ interface RalplanHudState {
15
+ stage?: string;
16
+ waiting?: string;
17
+ iteration?: number;
18
+ verdict?: string;
19
+ latestSummary?: string;
20
+ pendingApproval?: boolean;
21
+ updatedAt?: string;
22
+ }
23
+
24
+ interface UltragoalLikeGoal {
25
+ id: string;
26
+ title: string;
27
+ status: string;
28
+ }
29
+
30
+ interface UltragoalHudState {
31
+ status: string;
32
+ currentGoal?: UltragoalLikeGoal;
33
+ counts: Record<string, number>;
34
+ goals: UltragoalLikeGoal[];
35
+ latestLedgerEvent?: { event?: string; goalId?: string; timestamp?: string };
36
+ updatedAt?: string;
37
+ }
38
+
39
+ interface TeamHudWorker {
40
+ id: string;
41
+ status?: string;
42
+ }
43
+
44
+ interface TeamHudState {
45
+ phase: string;
46
+ task_total: number;
47
+ task_counts: Record<string, number>;
48
+ workers: TeamHudWorker[];
49
+ updated_at?: string;
50
+ latestEvent?: { type?: string; worker?: string; message?: string };
51
+ latestMessage?: { from_worker?: string; body?: string };
52
+ }
53
+
54
+ function percent(value: number | undefined): string | undefined {
55
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
56
+ return `${Math.round(value * 100)}%`;
57
+ }
58
+
59
+ function chip(
60
+ label: string,
61
+ value: string | undefined,
62
+ priority: number,
63
+ severity?: WorkflowHudChip["severity"],
64
+ ): WorkflowHudChip | null {
65
+ if (!value) return null;
66
+ return { label, value, priority, ...(severity ? { severity } : {}) };
67
+ }
68
+
69
+ function compactChips(chips: Array<WorkflowHudChip | null>): WorkflowHudChip[] {
70
+ return chips.filter((item): item is WorkflowHudChip => item !== null);
71
+ }
72
+
73
+ export function buildDeepInterviewHudSummary(state: DeepInterviewHudState): WorkflowHudSummary {
74
+ return {
75
+ version: 1,
76
+ chips: compactChips([
77
+ chip("phase", state.phase, 10),
78
+ chip("ambiguity", [percent(state.ambiguity), percent(state.threshold)].filter(Boolean).join("/"), 20),
79
+ chip("round", state.roundCount === undefined ? undefined : String(state.roundCount), 30),
80
+ chip("target", state.targetComponent, 40),
81
+ chip("weakest", state.weakestDimension, 50),
82
+ chip("spec", state.specStatus, 60),
83
+ ]),
84
+ ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
85
+ };
86
+ }
87
+
88
+ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSummary {
89
+ const verdict = state.verdict?.toUpperCase();
90
+ const verdictSeverity =
91
+ verdict === "BLOCK"
92
+ ? "blocked"
93
+ : verdict === "ITERATE" || verdict === "WATCH"
94
+ ? "warning"
95
+ : verdict === "APPROVE" || verdict === "CLEAR"
96
+ ? "success"
97
+ : undefined;
98
+ return {
99
+ version: 1,
100
+ summary: state.latestSummary,
101
+ chips: compactChips([
102
+ state.pendingApproval ? { label: "pending", value: "approval", priority: 5, severity: "warning" } : null,
103
+ chip("stage", state.stage, 10),
104
+ chip("waiting", state.waiting, 20),
105
+ chip("iter", state.iteration === undefined ? undefined : String(state.iteration), 30),
106
+ chip("verdict", verdict, 40, verdictSeverity),
107
+ ]),
108
+ ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
109
+ };
110
+ }
111
+
112
+ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudSummary {
113
+ const total = state.goals.length;
114
+ const complete = state.counts.complete ?? 0;
115
+ const blockers = (state.counts.blocked ?? 0) + (state.counts.review_blocked ?? 0) + (state.counts.failed ?? 0);
116
+ return {
117
+ version: 1,
118
+ chips: compactChips([
119
+ blockers > 0 ? { label: "blocked", value: String(blockers), priority: 5, severity: "blocked" } : null,
120
+ chip("goals", `${complete}/${total}`, 10),
121
+ chip("current", state.currentGoal ? `${state.currentGoal.id}:${state.currentGoal.title}` : state.status, 20),
122
+ chip("status", state.status, 30, state.status === "complete" ? "success" : undefined),
123
+ ]),
124
+ details: state.latestLedgerEvent
125
+ ? compactChips([
126
+ chip(
127
+ "ledger",
128
+ [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":"),
129
+ 100,
130
+ ),
131
+ ])
132
+ : undefined,
133
+ ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
134
+ };
135
+ }
136
+
137
+ export function buildTeamHudSummary(state: TeamHudState): WorkflowHudSummary {
138
+ const failedWorkers = state.workers.filter(
139
+ worker => worker.status === "failed" || worker.status === "blocked",
140
+ ).length;
141
+ const stoppedWorkers = state.workers.filter(worker => worker.status === "stopped").length;
142
+ const completed = state.task_counts.completed ?? 0;
143
+ const failedTasks = (state.task_counts.failed ?? 0) + (state.task_counts.blocked ?? 0);
144
+ const latest = state.latestEvent?.message ?? state.latestEvent?.type ?? state.latestMessage?.body;
145
+ return {
146
+ version: 1,
147
+ chips: compactChips([
148
+ failedWorkers > 0 || failedTasks > 0
149
+ ? { label: "blocked", value: String(failedWorkers + failedTasks), priority: 5, severity: "blocked" }
150
+ : stoppedWorkers > 0
151
+ ? { label: "stopped", value: String(stoppedWorkers), priority: 5, severity: "warning" }
152
+ : null,
153
+ chip("phase", state.phase, 10),
154
+ chip("workers", `${state.workers.length - failedWorkers}/${state.workers.length}`, 20),
155
+ chip("tasks", `${completed}/${state.task_total}`, 30),
156
+ chip("latest", latest, 50),
157
+ ]),
158
+ ...(state.updated_at ? { updated_at: state.updated_at } : {}),
159
+ };
160
+ }
@@ -44,6 +44,7 @@ interface ImageApiKey {
44
44
  apiKey: string;
45
45
  projectId?: string;
46
46
  model?: Model;
47
+ authCredentialType?: "api_key" | "oauth";
47
48
  }
48
49
 
49
50
  const responseModalitySchema = z.enum(["IMAGE", "TEXT"] as const);
@@ -441,6 +442,7 @@ async function findOpenAIHostedImageCredentials(
441
442
  provider: getOpenAIHostedImageProvider(activeModel),
442
443
  apiKey,
443
444
  model: activeModel,
445
+ authCredentialType: modelRegistry.getSessionCredentialType?.(activeModel.provider, sessionId),
444
446
  };
445
447
  }
446
448
 
@@ -700,16 +702,21 @@ function getOpenAIResponseErrorMessage(rawText: string): string {
700
702
  }
701
703
  }
702
704
 
703
- function getOpenAIBaseUrl(model: Model): string {
704
- const fallback =
705
- model.api === "openai-codex-responses" || model.provider === "openai-codex"
706
- ? CODEX_BASE_URL
707
- : DEFAULT_OPENAI_BASE_URL;
708
- return (model.baseUrl || fallback).replace(/\/+$/, "");
705
+ function getOpenAIBaseUrl(model: Model, authCredentialType?: "api_key" | "oauth"): string {
706
+ if (model.api === "openai-codex-responses" || model.provider === "openai-codex") {
707
+ return (model.baseUrl || CODEX_BASE_URL).replace(/\/+$/, "");
708
+ }
709
+ if (authCredentialType === "oauth") return DEFAULT_OPENAI_BASE_URL;
710
+ const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
711
+ const configuredBaseUrl = model.baseUrl?.trim();
712
+ if (envBaseUrl && (!configuredBaseUrl || configuredBaseUrl.toLowerCase().includes("api.openai.com"))) {
713
+ return envBaseUrl.replace(/\/+$/, "");
714
+ }
715
+ return (configuredBaseUrl || envBaseUrl || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
709
716
  }
710
717
 
711
- function getOpenAIResponsesUrl(model: Model): string {
712
- const baseUrl = getOpenAIBaseUrl(model);
718
+ function getOpenAIResponsesUrl(model: Model, authCredentialType?: "api_key" | "oauth"): string {
719
+ const baseUrl = getOpenAIBaseUrl(model, authCredentialType);
713
720
  if (model.api !== "openai-codex-responses" && model.provider !== "openai-codex") {
714
721
  return `${baseUrl}/responses`;
715
722
  }
@@ -782,11 +789,12 @@ async function generateOpenAIHostedImage(
782
789
  inputImages: InlineImageData[],
783
790
  signal: AbortSignal | undefined,
784
791
  sessionId: string | undefined,
792
+ options?: { authCredentialType?: "api_key" | "oauth" },
785
793
  ): Promise<OpenAIHostedImageResult> {
786
794
  const promptText = assemblePrompt(params);
787
795
  const stream = model.api === "openai-codex-responses" || model.provider === "openai-codex";
788
796
  const requestBody = buildOpenAIHostedImageRequest(model, promptText, params, inputImages, stream);
789
- const response = await fetch(getOpenAIResponsesUrl(model), {
797
+ const response = await fetch(getOpenAIResponsesUrl(model, options?.authCredentialType), {
790
798
  method: "POST",
791
799
  headers: buildOpenAIImageHeaders(model, apiKey, sessionId),
792
800
  body: JSON.stringify(requestBody),
@@ -946,6 +954,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
946
954
  resolvedImages,
947
955
  requestSignal,
948
956
  sessionId,
957
+ { authCredentialType: apiKey.authCredentialType },
949
958
  );
950
959
 
951
960
  if (parsed.images.length === 0) {
@@ -1075,7 +1084,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1075
1084
  headers: {
1076
1085
  "Content-Type": "application/json",
1077
1086
  Authorization: `Bearer ${apiKey.apiKey}`,
1078
- "HTTP-Referer": "https://gajae-code.dev/",
1087
+ "HTTP-Referer": "https://gaebal-gajae.dev/",
1079
1088
  "X-OpenRouter-Title": "Gajae Code",
1080
1089
  "X-OpenRouter-Categories": "cli-agent",
1081
1090
  },