@gajae-code/coding-agent 0.2.1 → 0.2.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 (101) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/types/commands/contribution-prep.d.ts +18 -0
  3. package/dist/types/commands/session.d.ts +24 -0
  4. package/dist/types/config/model-registry.d.ts +2 -2
  5. package/dist/types/config/models-config-schema.d.ts +17 -9
  6. package/dist/types/config/settings-schema.d.ts +1 -24
  7. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  8. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  9. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  12. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  15. package/dist/types/goals/runtime.d.ts +3 -9
  16. package/dist/types/goals/state.d.ts +3 -6
  17. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  18. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  19. package/dist/types/modes/components/status-line.d.ts +0 -3
  20. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  21. package/dist/types/modes/interactive-mode.d.ts +1 -12
  22. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  23. package/dist/types/modes/theme/theme.d.ts +1 -2
  24. package/dist/types/modes/types.d.ts +1 -7
  25. package/dist/types/session/agent-session.d.ts +2 -0
  26. package/dist/types/session/contribution-prep.d.ts +47 -0
  27. package/dist/types/skill-state/active-state.d.ts +4 -0
  28. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  29. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  30. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  31. package/package.json +7 -7
  32. package/src/cli/args.ts +3 -2
  33. package/src/cli.ts +6 -1
  34. package/src/commands/contribution-prep.ts +41 -0
  35. package/src/commands/deep-interview.ts +6 -22
  36. package/src/commands/launch.ts +10 -1
  37. package/src/commands/ralplan.ts +10 -22
  38. package/src/commands/session.ts +150 -0
  39. package/src/commands/state.ts +14 -4
  40. package/src/commands/team.ts +23 -3
  41. package/src/config/model-registry.ts +10 -2
  42. package/src/config/models-config-schema.ts +120 -102
  43. package/src/config/settings-schema.ts +1 -25
  44. package/src/config.ts +1 -1
  45. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  47. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  48. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  49. package/src/eval/py/prelude.py +1 -1
  50. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  51. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  52. package/src/gjc-runtime/launch-tmux.ts +83 -43
  53. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  54. package/src/gjc-runtime/state-runtime.ts +562 -0
  55. package/src/gjc-runtime/team-runtime.ts +708 -52
  56. package/src/gjc-runtime/tmux-common.ts +119 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  58. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  59. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  60. package/src/goals/runtime.ts +38 -144
  61. package/src/goals/state.ts +36 -7
  62. package/src/goals/tools/goal-tool.ts +15 -172
  63. package/src/hooks/skill-state.ts +31 -12
  64. package/src/internal-urls/docs-index.generated.ts +4 -3
  65. package/src/modes/components/skill-hud/render.ts +4 -0
  66. package/src/modes/components/status-line/segments.ts +5 -16
  67. package/src/modes/components/status-line/types.ts +0 -3
  68. package/src/modes/components/status-line.ts +0 -6
  69. package/src/modes/controllers/command-controller.ts +25 -1
  70. package/src/modes/controllers/input-controller.ts +0 -15
  71. package/src/modes/interactive-mode.ts +18 -219
  72. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  73. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  74. package/src/modes/theme/theme.ts +0 -6
  75. package/src/modes/types.ts +1 -7
  76. package/src/prompts/goals/goal-continuation.md +1 -4
  77. package/src/prompts/goals/goal-mode-active.md +3 -5
  78. package/src/prompts/system/system-prompt.md +5 -7
  79. package/src/prompts/tools/goal.md +4 -4
  80. package/src/sdk.ts +1 -1
  81. package/src/session/agent-session.ts +18 -0
  82. package/src/session/contribution-prep.ts +320 -0
  83. package/src/skill-state/active-state.ts +38 -0
  84. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  85. package/src/skill-state/workflow-hud.ts +23 -5
  86. package/src/skill-state/workflow-state-contract.ts +121 -0
  87. package/src/slash-commands/builtin-registry.ts +24 -12
  88. package/src/task/commands.ts +1 -5
  89. package/src/tools/gh.ts +212 -2
  90. package/src/tools/index.ts +2 -5
  91. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  92. package/dist/types/commands/question.d.ts +0 -7
  93. package/dist/types/modes/loop-limit.d.ts +0 -22
  94. package/src/commands/gjc-runtime-bridge.ts +0 -227
  95. package/src/commands/question.ts +0 -12
  96. package/src/modes/loop-limit.ts +0 -140
  97. package/src/prompts/commands/orchestrate.md +0 -49
  98. package/src/prompts/goals/goal-budget-limit.md +0 -16
  99. package/src/prompts/tools/create-goal.md +0 -3
  100. package/src/prompts/tools/get-goal.md +0 -3
  101. package/src/prompts/tools/update-goal.md +0 -3
package/src/tools/gh.ts CHANGED
@@ -225,6 +225,10 @@ const RUN_SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
225
225
  const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required", "startup_failure"]);
226
226
  const JOB_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required"]);
227
227
 
228
+ const PR_CREATE_BASE_CONFIG_KEYS = ["github.prBase", "gh.prBase", "gjc.github.prBase"] as const;
229
+ const ISSUE_CLOSING_REFERENCE_PATTERN =
230
+ /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:https:\/\/github\.com\/([^\s/]+\/[^\s/]+)\/issues\/)?#(\d+)\b/gi;
231
+
228
232
  const githubSchema = z
229
233
  .object({
230
234
  op: z
@@ -407,6 +411,23 @@ interface GhIssueViewData {
407
411
  updatedAt?: string;
408
412
  url?: string;
409
413
  }
414
+ interface GhPrListData {
415
+ baseRefName?: string;
416
+ headRefName?: string;
417
+ isDraft?: boolean;
418
+ number?: number;
419
+ state?: string;
420
+ title?: string;
421
+ url?: string;
422
+ }
423
+
424
+ interface PrCreateDuplicateCheck {
425
+ base: string;
426
+ head: string;
427
+ issue?: GhIssueViewData;
428
+ linkedPr?: GhPrListData;
429
+ pr?: GhPrListData;
430
+ }
410
431
 
411
432
  interface GhPrFile {
412
433
  path?: string;
@@ -659,6 +680,185 @@ function resolveSearchLimit(value: number | undefined): number {
659
680
 
660
681
  return Math.min(Math.floor(value), SEARCH_LIMIT_MAX);
661
682
  }
683
+ async function resolvePrCreateBase(
684
+ cwd: string,
685
+ explicitBase: string | undefined,
686
+ repo: string | undefined,
687
+ signal?: AbortSignal,
688
+ ): Promise<string | undefined> {
689
+ if (explicitBase) return explicitBase;
690
+ try {
691
+ for (const key of PR_CREATE_BASE_CONFIG_KEYS) {
692
+ const configured = normalizeOptionalString(await git.config.get(cwd, key, signal));
693
+ if (configured) return configured;
694
+ }
695
+ } catch {
696
+ // Repository config is optional for pr_create; prefer GitHub metadata when
697
+ // local git metadata is unavailable.
698
+ }
699
+ try {
700
+ const resolvedRepo = repo ?? (await resolveDefaultRepoMemoized(cwd, signal));
701
+ const repoView = await git.github.json<GhRepoViewData>(
702
+ cwd,
703
+ ["repo", "view", resolvedRepo, "--json", "defaultBranchRef"],
704
+ signal,
705
+ { repoProvided: true },
706
+ );
707
+ const defaultBranch = normalizeOptionalString(repoView.defaultBranchRef?.name);
708
+ if (defaultBranch) return defaultBranch;
709
+ } catch {
710
+ // Fall back to local git metadata below so pr_create can still work when gh
711
+ // cannot resolve repository metadata in otherwise-valid checkouts.
712
+ }
713
+ return (await git.branch.default(cwd, signal)) ?? undefined;
714
+ }
715
+
716
+ function normalizePrHead(value: string): string {
717
+ const separator = value.lastIndexOf(":");
718
+ return separator >= 0 ? value.slice(separator + 1) : value;
719
+ }
720
+
721
+ function extractClosingIssueReferences(body: string | undefined, repo: string | undefined): number[] {
722
+ if (!body) return [];
723
+ const issueNumbers: number[] = [];
724
+ ISSUE_CLOSING_REFERENCE_PATTERN.lastIndex = 0;
725
+ let match = ISSUE_CLOSING_REFERENCE_PATTERN.exec(body);
726
+ while (match !== null) {
727
+ const issueRepo = normalizeOptionalString(match[1]);
728
+ if (!issueRepo || !repo || issueRepo.toLowerCase() === repo.toLowerCase()) {
729
+ const issueNumber = Number(match[2]);
730
+ if (Number.isInteger(issueNumber) && issueNumber > 0) issueNumbers.push(issueNumber);
731
+ }
732
+ match = ISSUE_CLOSING_REFERENCE_PATTERN.exec(body);
733
+ }
734
+ return issueNumbers;
735
+ }
736
+
737
+ async function resolvePrCreateHead(
738
+ cwd: string,
739
+ explicitHead: string | undefined,
740
+ signal?: AbortSignal,
741
+ ): Promise<string | undefined> {
742
+ if (explicitHead) return explicitHead;
743
+ try {
744
+ return (await git.branch.current(cwd, signal)) ?? undefined;
745
+ } catch {
746
+ return undefined;
747
+ }
748
+ }
749
+
750
+ async function fetchPrCreateDuplicateCheck(
751
+ cwd: string,
752
+ repo: string | undefined,
753
+ base: string | undefined,
754
+ head: string | undefined,
755
+ body: string | undefined,
756
+ signal: AbortSignal | undefined,
757
+ ): Promise<PrCreateDuplicateCheck | undefined> {
758
+ if (!base || !head) return undefined;
759
+ const resolvedRepo = repo ?? (await resolveDefaultRepoMemoized(cwd, signal));
760
+ const normalizedHead = normalizePrHead(head);
761
+ const prs = await git.github.json<GhPrListData[]>(
762
+ cwd,
763
+ [
764
+ "pr",
765
+ "list",
766
+ "--repo",
767
+ resolvedRepo,
768
+ "--head",
769
+ normalizedHead,
770
+ "--base",
771
+ base,
772
+ "--state",
773
+ "all",
774
+ "--json",
775
+ "number,title,state,url,baseRefName,headRefName,isDraft",
776
+ ],
777
+ signal,
778
+ { repoProvided: true },
779
+ );
780
+ const existingPr = prs.find(pr => pr.headRefName === normalizedHead && pr.baseRefName === base) ?? prs[0];
781
+ const issueNumbers = existingPr ? [] : extractClosingIssueReferences(body, resolvedRepo);
782
+ let issue: GhIssueViewData | undefined;
783
+ let linkedPr: GhPrListData | undefined;
784
+ for (const issueNumber of issueNumbers) {
785
+ const candidateIssue = await git.github.json<GhIssueViewData>(
786
+ cwd,
787
+ [
788
+ "issue",
789
+ "view",
790
+ String(issueNumber),
791
+ "--repo",
792
+ resolvedRepo,
793
+ "--json",
794
+ GH_ISSUE_FIELDS_NO_COMMENTS.join(","),
795
+ ],
796
+ signal,
797
+ { repoProvided: true },
798
+ );
799
+ issue = candidateIssue;
800
+ if (candidateIssue.state && candidateIssue.state.toUpperCase() !== "OPEN") break;
801
+ const linkedPrs = await git.github.json<GhPrListData[]>(
802
+ cwd,
803
+ [
804
+ "pr",
805
+ "list",
806
+ "--repo",
807
+ resolvedRepo,
808
+ "--search",
809
+ `${issueNumber} linked:issue`,
810
+ "--state",
811
+ "open",
812
+ "--json",
813
+ "number,title,state,url,baseRefName,headRefName,isDraft",
814
+ ],
815
+ signal,
816
+ { repoProvided: true },
817
+ );
818
+ linkedPr = linkedPrs.find(pr => pr.headRefName !== normalizedHead) ?? linkedPrs[0];
819
+ if (linkedPr) break;
820
+ }
821
+ return { base, head: normalizedHead, issue, linkedPr, pr: existingPr };
822
+ }
823
+
824
+ function formatPrCreateExistingResult(check: PrCreateDuplicateCheck): string | undefined {
825
+ if (check.issue?.state && check.issue.state.toUpperCase() !== "OPEN") {
826
+ const lines = [
827
+ "# Pull Request Not Created",
828
+ "",
829
+ `Issue #${check.issue.number ?? ""} is ${check.issue.state.toLowerCase()}.`,
830
+ ];
831
+ pushLine(lines, "Issue", check.issue.url);
832
+ if (check.pr) pushLine(lines, "Existing PR", check.pr.url);
833
+ pushLine(lines, "Base", check.base);
834
+ pushLine(lines, "Head", check.head);
835
+ return lines.join("\n").trim();
836
+ }
837
+ if (check.linkedPr) {
838
+ const number = check.linkedPr.number !== undefined ? ` #${check.linkedPr.number}` : "";
839
+ const title = check.linkedPr.title ? `: ${check.linkedPr.title}` : "";
840
+ const lines = [`# Pull Request Already Linked${number}${title}`, ""];
841
+ pushLine(lines, "URL", check.linkedPr.url);
842
+ pushLine(lines, "Issue", check.issue?.url);
843
+ pushLine(lines, "State", check.linkedPr.state);
844
+ pushLine(lines, "Draft", check.linkedPr.isDraft);
845
+ pushLine(lines, "Base", check.linkedPr.baseRefName ?? check.base);
846
+ pushLine(lines, "Head", check.linkedPr.headRefName ?? check.head);
847
+ return lines.join("\n").trim();
848
+ }
849
+ if (check.pr) {
850
+ const number = check.pr.number !== undefined ? ` #${check.pr.number}` : "";
851
+ const title = check.pr.title ? `: ${check.pr.title}` : "";
852
+ const lines = [`# Pull Request Already Exists${number}${title}`, ""];
853
+ pushLine(lines, "URL", check.pr.url);
854
+ pushLine(lines, "State", check.pr.state);
855
+ pushLine(lines, "Draft", check.pr.isDraft);
856
+ pushLine(lines, "Base", check.pr.baseRefName ?? check.base);
857
+ pushLine(lines, "Head", check.pr.headRefName ?? check.head);
858
+ return lines.join("\n").trim();
859
+ }
860
+ return undefined;
861
+ }
662
862
 
663
863
  function resolveTailLimit(value: number | undefined): number {
664
864
  if (value === undefined) {
@@ -3117,8 +3317,8 @@ async function executePrCreate(
3117
3317
  const repo = normalizeOptionalString(params.repo);
3118
3318
  const title = normalizeOptionalString(params.title);
3119
3319
  const body = params.body;
3120
- const base = normalizeOptionalString(params.base);
3121
- const head = normalizeOptionalString(params.head);
3320
+ const requestedBase = normalizeOptionalString(params.base);
3321
+ const requestedHead = normalizeOptionalString(params.head);
3122
3322
  const draft = params.draft ?? false;
3123
3323
  const fill = params.fill ?? false;
3124
3324
  const reviewers = normalizePrIdentifierList(params.reviewer);
@@ -3131,6 +3331,16 @@ async function executePrCreate(
3131
3331
  if (fill && (title || body !== undefined)) {
3132
3332
  throw new ToolError("fill is mutually exclusive with title and body");
3133
3333
  }
3334
+ const base = await resolvePrCreateBase(session.cwd, requestedBase, repo, signal);
3335
+ const head = await resolvePrCreateHead(session.cwd, requestedHead, signal);
3336
+ const duplicateCheck = await fetchPrCreateDuplicateCheck(session.cwd, repo, base, head, body, signal);
3337
+ const existingText = duplicateCheck ? formatPrCreateExistingResult(duplicateCheck) : undefined;
3338
+ if (existingText) {
3339
+ return buildTextResult(
3340
+ existingText,
3341
+ duplicateCheck?.pr?.url ?? duplicateCheck?.linkedPr?.url ?? duplicateCheck?.issue?.url,
3342
+ );
3343
+ }
3134
3344
 
3135
3345
  const args = ["pr", "create"];
3136
3346
  appendRepoFlag(args, repo);
@@ -7,7 +7,7 @@ import { EditTool } from "../edit";
7
7
  import { checkPythonKernelAvailability } from "../eval/py/kernel";
8
8
  import type { Skill } from "../extensibility/skills";
9
9
  import type { GoalModeState, GoalRuntime } from "../goals";
10
- import { CreateGoalTool, GetGoalTool, GoalTool, UpdateGoalTool } from "../goals/tools/goal-tool";
10
+ import { GoalTool } from "../goals/tools/goal-tool";
11
11
  import type { HindsightSessionState } from "../hindsight/state";
12
12
  import { LspTool } from "../lsp";
13
13
  import type { PlanModeState } from "../plan-mode/state";
@@ -309,12 +309,9 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
309
309
  recall: HindsightRecallTool.createIf,
310
310
  reflect: HindsightReflectTool.createIf,
311
311
  goal: s => new GoalTool(s),
312
- get_goal: GetGoalTool.createIf,
313
- create_goal: CreateGoalTool.createIf,
314
- update_goal: UpdateGoalTool.createIf,
315
312
  };
316
313
 
317
- const GOAL_MODE_TOOL_NAMES = ["get_goal", "create_goal", "update_goal"] as const;
314
+ const GOAL_MODE_TOOL_NAMES = [] as const;
318
315
 
319
316
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
320
317
  yield: s => new YieldTool(s),
@@ -1,30 +0,0 @@
1
- import { type WorkflowHudSummary } from "../skill-state/active-state";
2
- export declare const WORKFLOW_HUD_PROTOCOL = "workflow-hud-summary-v1";
3
- export interface GjcRuntimeBridgeResult {
4
- status: number;
5
- error?: string;
6
- }
7
- export interface WorkflowHudBridgePayload {
8
- version: 1;
9
- skill: string;
10
- phase?: string;
11
- active?: boolean;
12
- session_id?: string;
13
- thread_id?: string;
14
- turn_id?: string;
15
- hud: WorkflowHudSummary;
16
- }
17
- export interface GjcRuntimeHudBridgeResult extends GjcRuntimeBridgeResult {
18
- hudPayload?: WorkflowHudBridgePayload;
19
- }
20
- export interface GjcRuntimeHudBridgeOptions {
21
- cwd?: string;
22
- env?: NodeJS.ProcessEnv;
23
- sidecarSkill: string;
24
- onHudPayload?: (payload: WorkflowHudBridgePayload) => Promise<void> | void;
25
- pollIntervalMs?: number;
26
- }
27
- export declare function normalizeWorkflowHudBridgePayload(raw: unknown, expectedSkill: string): WorkflowHudBridgePayload | null;
28
- export declare function runGjcRuntimeBridge(endpoint: string, args: string[], env?: NodeJS.ProcessEnv): GjcRuntimeBridgeResult;
29
- export declare function runGjcRuntimeBridgeWithHudSidecar(endpoint: string, args: string[], options: GjcRuntimeHudBridgeOptions): Promise<GjcRuntimeHudBridgeResult>;
30
- export declare function runBridgedRuntimeEndpoint(endpoint: string, args: string[]): Promise<void>;
@@ -1,7 +0,0 @@
1
- import { Command } from "@gajae-code/utils/cli";
2
- export default class Question extends Command {
3
- static description: string;
4
- static strict: boolean;
5
- static examples: string[];
6
- run(): Promise<void>;
7
- }
@@ -1,22 +0,0 @@
1
- export type LoopLimitConfig = {
2
- kind: "iterations";
3
- iterations: number;
4
- } | {
5
- kind: "duration";
6
- durationMs: number;
7
- };
8
- export type LoopLimitRuntime = {
9
- kind: "iterations";
10
- initial: number;
11
- remaining: number;
12
- } | {
13
- kind: "duration";
14
- durationMs: number;
15
- deadlineMs: number;
16
- };
17
- export declare function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string;
18
- export declare function createLoopLimitRuntime(config: LoopLimitConfig | undefined, nowMs?: number): LoopLimitRuntime | undefined;
19
- export declare function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
20
- export declare function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
21
- export declare function describeLoopLimit(config: LoopLimitConfig): string;
22
- export declare function describeLoopLimitRuntime(limit: LoopLimitRuntime): string;
@@ -1,227 +0,0 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import { existsSync } from "node:fs";
4
- import * as fs from "node:fs/promises";
5
- import * as os from "node:os";
6
- import * as path from "node:path";
7
- import { normalizeWorkflowHudSummary, type WorkflowHudSummary } from "../skill-state/active-state";
8
-
9
- const BRIDGE_ENV = "GJC_RUNTIME_BINARY";
10
- const LEGACY_BRIDGE_ENV = "GJC_LEGACY_RUNTIME_BINARY";
11
- const GUARD_ENV = "GJC_RUNTIME_BRIDGE_ACTIVE";
12
- export const WORKFLOW_HUD_PROTOCOL = "workflow-hud-summary-v1";
13
-
14
- export interface GjcRuntimeBridgeResult {
15
- status: number;
16
- error?: string;
17
- }
18
-
19
- const SKILL_ENTRYPOINT_ENDPOINTS = new Set(["deep-interview", "ralplan"]);
20
-
21
- export interface WorkflowHudBridgePayload {
22
- version: 1;
23
- skill: string;
24
- phase?: string;
25
- active?: boolean;
26
- session_id?: string;
27
- thread_id?: string;
28
- turn_id?: string;
29
- hud: WorkflowHudSummary;
30
- }
31
-
32
- export interface GjcRuntimeHudBridgeResult extends GjcRuntimeBridgeResult {
33
- hudPayload?: WorkflowHudBridgePayload;
34
- }
35
-
36
- export interface GjcRuntimeHudBridgeOptions {
37
- cwd?: string;
38
- env?: NodeJS.ProcessEnv;
39
- sidecarSkill: string;
40
- onHudPayload?: (payload: WorkflowHudBridgePayload) => Promise<void> | void;
41
- pollIntervalMs?: number;
42
- }
43
-
44
- function candidateBinaries(env: NodeJS.ProcessEnv): string[] {
45
- return [env[BRIDGE_ENV], env[LEGACY_BRIDGE_ENV]].filter(
46
- (value): value is string => typeof value === "string" && value.trim().length > 0,
47
- );
48
- }
49
-
50
- function isPathLike(command: string): boolean {
51
- return command.includes("/") || command.includes("\\");
52
- }
53
-
54
- function canAttempt(command: string): boolean {
55
- return !isPathLike(command) || existsSync(command);
56
- }
57
-
58
- function unavailableBridgeResult(
59
- endpoint: string,
60
- env: NodeJS.ProcessEnv,
61
- attempted: string[],
62
- ): GjcRuntimeBridgeResult {
63
- const configured = [env[BRIDGE_ENV], env[LEGACY_BRIDGE_ENV]].filter(Boolean).join(", ");
64
- const guidance = SKILL_ENTRYPOINT_ENDPOINTS.has(endpoint)
65
- ? `Inside a GJC agent session, invoke /skill:${endpoint} instead so the bundled skill is loaded directly.`
66
- : `Configure ${BRIDGE_ENV} with a GJC-compatible private runtime binary for the ${endpoint} endpoint.`;
67
- return {
68
- status: 1,
69
- error: [
70
- `gjc ${endpoint} is a private runtime bridge command.`,
71
- guidance,
72
- `Only private runtime deployments should call this bridge command; configure them with ${BRIDGE_ENV}.`,
73
- configured
74
- ? `Configured runtime candidates failed: ${configured}.`
75
- : "No private GJC runtime binary was configured.",
76
- attempted.length > 0 ? `Attempted: ${attempted.join(", ")}.` : undefined,
77
- ]
78
- .filter(Boolean)
79
- .join("\n"),
80
- };
81
- }
82
-
83
- export function normalizeWorkflowHudBridgePayload(
84
- raw: unknown,
85
- expectedSkill: string,
86
- ): WorkflowHudBridgePayload | null {
87
- if (!raw || typeof raw !== "object") return null;
88
- const record = raw as Record<string, unknown>;
89
- if (record.version !== 1 || record.skill !== expectedSkill) return null;
90
- const hud = normalizeWorkflowHudSummary(record.hud);
91
- if (!hud) return null;
92
- return {
93
- version: 1,
94
- skill: expectedSkill,
95
- phase: typeof record.phase === "string" && record.phase.trim() ? record.phase.trim() : undefined,
96
- active: typeof record.active === "boolean" ? record.active : undefined,
97
- session_id:
98
- typeof record.session_id === "string" && record.session_id.trim() ? record.session_id.trim() : undefined,
99
- thread_id: typeof record.thread_id === "string" && record.thread_id.trim() ? record.thread_id.trim() : undefined,
100
- turn_id: typeof record.turn_id === "string" && record.turn_id.trim() ? record.turn_id.trim() : undefined,
101
- hud,
102
- };
103
- }
104
-
105
- async function readHudPayload(sidecarPath: string, expectedSkill: string): Promise<WorkflowHudBridgePayload | null> {
106
- try {
107
- return normalizeWorkflowHudBridgePayload(JSON.parse(await Bun.file(sidecarPath).text()), expectedSkill);
108
- } catch {
109
- return null;
110
- }
111
- }
112
-
113
- export function runGjcRuntimeBridge(
114
- endpoint: string,
115
- args: string[],
116
- env: NodeJS.ProcessEnv = process.env,
117
- ): GjcRuntimeBridgeResult {
118
- if (env[GUARD_ENV] === "1") {
119
- return {
120
- status: 1,
121
- error: `Refusing recursive gjc runtime bridge for ${endpoint}.`,
122
- };
123
- }
124
-
125
- const attempted: string[] = [];
126
- for (const binary of candidateBinaries(env)) {
127
- const command = binary.trim();
128
- if (!canAttempt(command)) continue;
129
- attempted.push(command);
130
- const child = spawnSync(command, [endpoint, ...args], {
131
- stdio: "inherit",
132
- env: {
133
- ...env,
134
- [GUARD_ENV]: "1",
135
- },
136
- });
137
-
138
- if (child.error) {
139
- const error = child.error as NodeJS.ErrnoException;
140
- if (error.code === "ENOENT") continue;
141
- return { status: 1, error: error.message };
142
- }
143
-
144
- return { status: child.status ?? (child.signal ? 1 : 0) };
145
- }
146
-
147
- return unavailableBridgeResult(endpoint, env, attempted);
148
- }
149
-
150
- export async function runGjcRuntimeBridgeWithHudSidecar(
151
- endpoint: string,
152
- args: string[],
153
- options: GjcRuntimeHudBridgeOptions,
154
- ): Promise<GjcRuntimeHudBridgeResult> {
155
- const env = options.env ?? process.env;
156
- if (env[GUARD_ENV] === "1") return { status: 1, error: `Refusing recursive gjc runtime bridge for ${endpoint}.` };
157
-
158
- const attempted: string[] = [];
159
- for (const binary of candidateBinaries(env)) {
160
- const command = binary.trim();
161
- if (!canAttempt(command)) continue;
162
- attempted.push(command);
163
- const sidecarDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-workflow-hud-"));
164
- const sidecarPath = path.join(sidecarDir, `${options.sidecarSkill}-${randomUUID()}.json`);
165
- let latestPayload: WorkflowHudBridgePayload | undefined;
166
- let lastRaw = "";
167
- const publishPayload = async (): Promise<void> => {
168
- let raw = "";
169
- try {
170
- raw = await Bun.file(sidecarPath).text();
171
- } catch {
172
- return;
173
- }
174
- if (!raw || raw === lastRaw) return;
175
- lastRaw = raw;
176
- const payload = await readHudPayload(sidecarPath, options.sidecarSkill);
177
- if (!payload) return;
178
- latestPayload = payload;
179
- try {
180
- await options.onHudPayload?.(payload);
181
- } catch {
182
- // HUD sidecar sync is best-effort and must not change child command semantics.
183
- }
184
- };
185
- try {
186
- const child = spawn(command, [endpoint, ...args], {
187
- cwd: options.cwd,
188
- stdio: "inherit",
189
- env: {
190
- ...env,
191
- [GUARD_ENV]: "1",
192
- GJC_WORKFLOW_HUD_PROTOCOL: WORKFLOW_HUD_PROTOCOL,
193
- GJC_WORKFLOW_HUD_SIDECAR: sidecarPath,
194
- GJC_WORKFLOW_HUD_SKILL: options.sidecarSkill,
195
- },
196
- });
197
- const interval = setInterval(() => {
198
- void publishPayload();
199
- }, options.pollIntervalMs ?? 100);
200
- const exit = Promise.withResolvers<{ status: number; error?: string; code?: string }>();
201
- child.on("error", error => {
202
- const fsError = error as NodeJS.ErrnoException;
203
- exit.resolve({ status: 1, error: error.message, code: fsError.code });
204
- });
205
- child.on("exit", (code, signal) => exit.resolve({ status: code ?? (signal ? 1 : 0) }));
206
- const result = await exit.promise;
207
- clearInterval(interval);
208
- await publishPayload();
209
- if (result.code === "ENOENT") continue;
210
- return {
211
- status: result.status,
212
- ...(result.error ? { error: result.error } : {}),
213
- ...(latestPayload ? { hudPayload: latestPayload } : {}),
214
- };
215
- } finally {
216
- await fs.rm(sidecarDir, { recursive: true, force: true });
217
- }
218
- }
219
-
220
- return unavailableBridgeResult(endpoint, env, attempted);
221
- }
222
-
223
- export async function runBridgedRuntimeEndpoint(endpoint: string, args: string[]): Promise<void> {
224
- const result = runGjcRuntimeBridge(endpoint, args);
225
- if (result.error) process.stderr.write(`${result.error}\n`);
226
- process.exitCode = result.status;
227
- }
@@ -1,12 +0,0 @@
1
- import { Command } from "@gajae-code/utils/cli";
2
- import { runBridgedRuntimeEndpoint } from "./gjc-runtime-bridge";
3
-
4
- export default class Question extends Command {
5
- static description = "Ask a blocking private runtime question through the GJC bridge";
6
- static strict = false;
7
- static examples = ["$ gjc question --input '<json>' --json"];
8
-
9
- async run(): Promise<void> {
10
- await runBridgedRuntimeEndpoint("question", this.argv);
11
- }
12
- }