@gajae-code/coding-agent 0.3.2 → 0.4.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 (122) hide show
  1. package/CHANGELOG.md +32 -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 +9 -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/discovery/claude-plugins.ts +25 -5
  58. package/src/edit/diff.ts +64 -1
  59. package/src/edit/modes/replace.ts +60 -2
  60. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  61. package/src/extensibility/gjc-plugins/index.ts +9 -0
  62. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  63. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  64. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  65. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  66. package/src/extensibility/gjc-plugins/state.ts +29 -0
  67. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  68. package/src/extensibility/gjc-plugins/types.ts +97 -0
  69. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  70. package/src/extensibility/skills.ts +39 -7
  71. package/src/gjc-runtime/state-runtime.ts +93 -2
  72. package/src/gjc-runtime/state-writer.ts +17 -1
  73. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  74. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  75. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  76. package/src/harness-control-plane/storage.ts +144 -2
  77. package/src/hashline/hash.ts +23 -0
  78. package/src/hooks/skill-state.ts +2 -0
  79. package/src/internal-urls/docs-index.generated.ts +5 -5
  80. package/src/lsp/client.ts +7 -0
  81. package/src/modes/acp/acp-agent.ts +25 -2
  82. package/src/modes/bridge/bridge-mode.ts +124 -2
  83. package/src/modes/controllers/input-controller.ts +14 -2
  84. package/src/modes/prompt-action-autocomplete.ts +49 -10
  85. package/src/modes/rpc/rpc-client.ts +57 -3
  86. package/src/modes/rpc/rpc-mode.ts +67 -0
  87. package/src/modes/rpc/rpc-types.ts +224 -2
  88. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  89. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  90. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  91. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  92. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  93. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  94. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  95. package/src/modes/shared/agent-wire/responses.ts +2 -2
  96. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  97. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  98. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  99. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  101. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  102. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  103. package/src/modes/theme/theme.ts +6 -0
  104. package/src/runtime-mcp/client.ts +7 -4
  105. package/src/runtime-mcp/manager.ts +45 -13
  106. package/src/runtime-mcp/transports/http.ts +40 -14
  107. package/src/runtime-mcp/transports/stdio.ts +11 -10
  108. package/src/sdk.ts +47 -0
  109. package/src/session/agent-session.ts +211 -2
  110. package/src/session/blob-store.ts +84 -0
  111. package/src/session/messages.ts +3 -0
  112. package/src/session/session-manager.ts +390 -33
  113. package/src/session/session-storage.ts +26 -0
  114. package/src/setup/provider-onboarding.ts +2 -2
  115. package/src/skill-state/active-state.ts +89 -1
  116. package/src/task/discovery.ts +7 -1
  117. package/src/task/executor.ts +16 -2
  118. package/src/thinking.ts +8 -2
  119. package/src/tools/ask.ts +39 -9
  120. package/src/tools/index.ts +3 -0
  121. package/src/tools/skill.ts +15 -3
  122. package/src/utils/edit-mode.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { THINKING_EFFORTS } from "@gajae-code/ai";
1
+ import { THINKING_EFFORTS } from "@gajae-code/ai/model-thinking";
2
2
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
3
  import { getThinkingLevelMetadata } from "../thinking";
4
4
  import { EDIT_MODES } from "../utils/edit-mode";
@@ -2775,6 +2775,8 @@ export const SETTINGS_SCHEMA = {
2775
2775
  "thinkingBudgets.high": { type: "number", default: 16384 },
2776
2776
 
2777
2777
  "thinkingBudgets.xhigh": { type: "number", default: 32768 },
2778
+
2779
+ "thinkingBudgets.max": { type: "number", default: 65536 },
2778
2780
  } as const;
2779
2781
 
2780
2782
  // ═══════════════════════════════════════════════════════════════════════════
@@ -2955,6 +2957,7 @@ export interface ThinkingBudgetsSettings {
2955
2957
  medium: number;
2956
2958
  high: number;
2957
2959
  xhigh: number;
2960
+ max: number;
2958
2961
  }
2959
2962
 
2960
2963
  export interface SttSettings {
@@ -13,6 +13,7 @@ import { type Skill, skillCapability } from "../capability/skill";
13
13
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
14
14
  import { type CustomTool, toolCapability } from "../capability/tool";
15
15
  import type { LoadContext, LoadResult } from "../capability/types";
16
+ import { rootContainsGjcManifest } from "../extensibility/gjc-plugins";
16
17
  import {
17
18
  type ClaudePluginRoot,
18
19
  createSourceMeta,
@@ -91,6 +92,25 @@ async function resolvePluginDir(
91
92
  };
92
93
  }
93
94
 
95
+ async function listNonGjcPluginRoots(
96
+ home: string,
97
+ cwd: string,
98
+ ): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
99
+ const { roots, warnings } = await listClaudePluginRoots(home, cwd);
100
+ const filteredRoots: ClaudePluginRoot[] = [];
101
+ const filteredWarnings = [...warnings];
102
+
103
+ for (const root of roots) {
104
+ if (await rootContainsGjcManifest(root.path)) {
105
+ filteredWarnings.push(`[claude-plugins] Skipping gajae-code plugin root (binding-only): ${root.path}`);
106
+ continue;
107
+ }
108
+ filteredRoots.push(root);
109
+ }
110
+
111
+ return { roots: filteredRoots, warnings: filteredWarnings };
112
+ }
113
+
94
114
  // =============================================================================
95
115
  // Skills
96
116
  // =============================================================================
@@ -99,7 +119,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
99
119
  const items: Skill[] = [];
100
120
  const warnings: string[] = [];
101
121
 
102
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
122
+ const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd);
103
123
  warnings.push(...rootWarnings);
104
124
 
105
125
  const results = await Promise.all(
@@ -134,7 +154,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
134
154
  const items: SlashCommand[] = [];
135
155
  const warnings: string[] = [];
136
156
 
137
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
157
+ const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd);
138
158
  warnings.push(...rootWarnings);
139
159
 
140
160
  const results = await Promise.all(
@@ -174,7 +194,7 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
174
194
  const items: Hook[] = [];
175
195
  const warnings: string[] = [];
176
196
 
177
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
197
+ const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd);
178
198
  warnings.push(...rootWarnings);
179
199
 
180
200
  const hookTypes = ["pre", "post"] as const;
@@ -221,7 +241,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
221
241
  const items: CustomTool[] = [];
222
242
  const warnings: string[] = [];
223
243
 
224
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
244
+ const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd);
225
245
  warnings.push(...rootWarnings);
226
246
 
227
247
  const results = await Promise.all(
@@ -258,7 +278,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
258
278
  const items: MCPServer[] = [];
259
279
  const warnings: string[] = [];
260
280
 
261
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
281
+ const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd);
262
282
  warnings.push(...rootWarnings);
263
283
 
264
284
  for (const root of roots) {
package/src/edit/diff.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * Provides diff string generation and the replace-mode edit logic
5
5
  * used when not in patch mode.
6
6
  */
7
+
8
+ import { createRequire } from "node:module";
7
9
  import * as Diff from "diff";
8
10
  import { resolveToCwd } from "../tools/path-utils";
9
11
  import { DEFAULT_FUZZY_THRESHOLD, EditMatchError, findMatch } from "./modes/replace";
@@ -54,12 +56,73 @@ function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, conten
54
56
  return `${prefix}${lineNum}|${content}`;
55
57
  }
56
58
 
59
+ type DiffLinePart = {
60
+ added?: boolean;
61
+ removed?: boolean;
62
+ value: string;
63
+ };
64
+
65
+ type DiffLinesFn = (oldStr: string, newStr: string) => DiffLinePart[];
66
+
67
+ const require = createRequire(import.meta.url);
68
+ const DIFF_LINES_TEST_OVERRIDE_UNSET = Symbol("DIFF_LINES_TEST_OVERRIDE_UNSET");
69
+
70
+ let cachedNativeDiffLines: DiffLinesFn | null | undefined;
71
+ let diffLinesTestOverride: DiffLinesFn | null | typeof DIFF_LINES_TEST_OVERRIDE_UNSET = DIFF_LINES_TEST_OVERRIDE_UNSET;
72
+
73
+ function resolveNativeDiffLines(): DiffLinesFn | undefined {
74
+ if (diffLinesTestOverride !== DIFF_LINES_TEST_OVERRIDE_UNSET) {
75
+ return diffLinesTestOverride ?? undefined;
76
+ }
77
+
78
+ if (cachedNativeDiffLines !== undefined) {
79
+ return cachedNativeDiffLines ?? undefined;
80
+ }
81
+
82
+ try {
83
+ const natives = require("@gajae-code/natives") as { diffLines?: unknown };
84
+ cachedNativeDiffLines = typeof natives.diffLines === "function" ? (natives.diffLines as DiffLinesFn) : null;
85
+ } catch {
86
+ cachedNativeDiffLines = null;
87
+ }
88
+
89
+ return cachedNativeDiffLines ?? undefined;
90
+ }
91
+
92
+ function diffLinesWithFallback(oldContent: string, newContent: string): DiffLinePart[] {
93
+ const nativeDiffLines = resolveNativeDiffLines();
94
+ if (nativeDiffLines) {
95
+ try {
96
+ return nativeDiffLines(oldContent, newContent);
97
+ } catch {
98
+ // Fall through to the JS implementation if the native export fails at runtime.
99
+ }
100
+ }
101
+
102
+ return Diff.diffLines(oldContent, newContent);
103
+ }
104
+
105
+ export function __setDiffLinesForTest(diffLines: DiffLinesFn | null): void {
106
+ diffLinesTestOverride = diffLines;
107
+ }
108
+
109
+ export function __clearDiffLinesForTest(): void {
110
+ diffLinesTestOverride = DIFF_LINES_TEST_OVERRIDE_UNSET;
111
+ cachedNativeDiffLines = undefined;
112
+ }
113
+
114
+ export function __getNativeDiffLinesForTest(): DiffLinesFn | undefined {
115
+ return resolveNativeDiffLines();
116
+ }
117
+
57
118
  /**
58
119
  * Generate a unified diff string with line numbers and context.
59
120
  * Returns both the diff string and the first changed line number (in the new file).
60
121
  */
61
122
  export function generateDiffString(oldContent: string, newContent: string, contextLines = 4): DiffResult {
62
- const parts = Diff.diffLines(oldContent, newContent);
123
+ // Native line diff (Rust port of jsdiff `Diff.diffLines`, byte-identical
124
+ // output) — avoids the pure-JS Myers blowup (>1s on ~1MB files).
125
+ const parts = diffLinesWithFallback(oldContent, newContent);
63
126
  const output: string[] = [];
64
127
 
65
128
  let oldLineNum = 1;
@@ -21,6 +21,40 @@ import {
21
21
  restoreLineEndings,
22
22
  stripBom,
23
23
  } from "../normalize";
24
+
25
+ type NativeBestFuzzyMatchResult = {
26
+ best?: FuzzyMatch;
27
+ aboveThresholdCount: number;
28
+ secondBestScore: number;
29
+ };
30
+
31
+ type NativeSequenceFuzzyResult = {
32
+ index?: number;
33
+ confidence: number;
34
+ matchCount: number;
35
+ matchIndices: number[];
36
+ secondBestScore: number;
37
+ };
38
+
39
+ let scoreSequenceFuzzyNative:
40
+ | ((lines: string[], pattern: string[], start: number, eof: boolean) => NativeSequenceFuzzyResult)
41
+ | undefined;
42
+ let findBestFuzzyMatchNative:
43
+ | ((content: string, target: string, threshold: number) => NativeBestFuzzyMatchResult)
44
+ | undefined;
45
+ void import("../../../../natives/native/index.js")
46
+ .then(mod => {
47
+ if (typeof mod.h02ScoreSequenceFuzzy === "function") {
48
+ scoreSequenceFuzzyNative = mod.h02ScoreSequenceFuzzy;
49
+ }
50
+ if (typeof mod.h01FindBestFuzzyMatch === "function") {
51
+ findBestFuzzyMatchNative = mod.h01FindBestFuzzyMatch;
52
+ }
53
+ })
54
+ .catch(() => {
55
+ // Native unavailable; fuzzy matching uses the TS fallback.
56
+ });
57
+
24
58
  import { readEditFileText, serializeEditFileText } from "../read-file";
25
59
  import type { EditToolDetails, LspBatchRequest } from "../renderer";
26
60
 
@@ -444,7 +478,7 @@ function findBestFuzzyMatchCore(
444
478
  return { best, aboveThresholdCount, secondBestScore };
445
479
  }
446
480
 
447
- function findBestFuzzyMatch(content: string, target: string, threshold: number): BestFuzzyMatchResult {
481
+ export function findBestFuzzyMatch(content: string, target: string, threshold: number): BestFuzzyMatchResult {
448
482
  const contentLines = content.split("\n");
449
483
  const targetLines = target.split("\n");
450
484
 
@@ -489,7 +523,8 @@ export function findMatch(
489
523
 
490
524
  // Try fuzzy match
491
525
  const threshold = options.threshold ?? DEFAULT_FUZZY_THRESHOLD;
492
- const { best, aboveThresholdCount, secondBestScore } = findBestFuzzyMatch(content, target, threshold);
526
+ const { best, aboveThresholdCount, secondBestScore } =
527
+ findBestFuzzyMatchNative?.(content, target, threshold) ?? findBestFuzzyMatch(content, target, threshold);
493
528
 
494
529
  if (!best) {
495
530
  return {};
@@ -682,6 +717,29 @@ export function seekSequence(
682
717
  return { index: undefined, confidence: 0 };
683
718
  }
684
719
 
720
+ const nativeFuzzyResult = scoreSequenceFuzzyNative?.(lines, pattern, start, eof);
721
+ if (nativeFuzzyResult?.index !== undefined && nativeFuzzyResult.confidence >= SEQUENCE_FUZZY_THRESHOLD) {
722
+ if (
723
+ nativeFuzzyResult.matchCount > 1 &&
724
+ nativeFuzzyResult.confidence >= DOMINANT_FUZZY_MIN_CONFIDENCE &&
725
+ nativeFuzzyResult.confidence - nativeFuzzyResult.secondBestScore >= DOMINANT_FUZZY_DELTA
726
+ ) {
727
+ return {
728
+ index: nativeFuzzyResult.index,
729
+ confidence: nativeFuzzyResult.confidence,
730
+ matchCount: 1,
731
+ matchIndices: nativeFuzzyResult.matchIndices,
732
+ strategy: "fuzzy-dominant",
733
+ };
734
+ }
735
+ return {
736
+ index: nativeFuzzyResult.index,
737
+ confidence: nativeFuzzyResult.confidence,
738
+ matchCount: nativeFuzzyResult.matchCount,
739
+ matchIndices: nativeFuzzyResult.matchIndices,
740
+ strategy: "fuzzy",
741
+ };
742
+ }
685
743
  // Pass 7: Fuzzy matching - find best match above threshold
686
744
  let bestScore = 0;
687
745
  let secondBestScore = 0;
@@ -0,0 +1,87 @@
1
+ import { logger } from "@gajae-code/utils";
2
+ import { loadGjcPlugins } from "./loader";
3
+ import { discoverGjcPluginRoots } from "./paths";
4
+ import { GjcPluginLoadError, type LoadedGjcPlugin, type LoadedSubskillActivation } from "./types";
5
+
6
+ export interface SubskillActivationResult {
7
+ cleanedArgs: string;
8
+ activation?: LoadedSubskillActivation;
9
+ activeSubskillsToPersist: LoadedSubskillActivation[];
10
+ }
11
+
12
+ export async function resolveSubskillActivationForSkillInvocation(input: {
13
+ cwd: string;
14
+ sessionId?: string;
15
+ threadId?: string;
16
+ turnId?: string;
17
+ skillName: string;
18
+ args: string;
19
+ }): Promise<SubskillActivationResult> {
20
+ const roots = await discoverGjcPluginRoots({ cwd: input.cwd });
21
+ let plugins: LoadedGjcPlugin[];
22
+ try {
23
+ plugins = await loadGjcPlugins(roots);
24
+ } catch (error) {
25
+ if (error instanceof GjcPluginLoadError) throw error;
26
+ logger.warn("Skipping GJC plugin activation set after load error", {
27
+ error: error instanceof Error ? error.message : String(error),
28
+ });
29
+ plugins = [];
30
+ }
31
+
32
+ const bindings = plugins.flatMap(plugin => plugin.bindings);
33
+ const activationsByArg = new Map<string, LoadedSubskillActivation>();
34
+ for (const binding of bindings) {
35
+ if (binding.parent !== input.skillName) continue;
36
+ activationsByArg.set(binding.activationArg, {
37
+ activationArg: binding.activationArg,
38
+ plugin: binding.plugin,
39
+ subskillName: binding.subskillName,
40
+ parent: binding.parent,
41
+ bindsTo: binding.bindsTo,
42
+ phase: binding.phase,
43
+ filePath: binding.filePath,
44
+ toolPaths: binding.toolPaths,
45
+ });
46
+ }
47
+
48
+ const tokens = input.args
49
+ .trim()
50
+ .split(/\s+/)
51
+ .filter(token => token.length > 0);
52
+ let activation: LoadedSubskillActivation | undefined;
53
+ const cleanedTokens: string[] = [];
54
+ let consumed = false;
55
+ for (const token of tokens) {
56
+ if (!consumed && token.startsWith("--") && !token.includes("=")) {
57
+ const candidate = activationsByArg.get(token.slice(2));
58
+ if (candidate) {
59
+ activation = candidate;
60
+ consumed = true;
61
+ continue;
62
+ }
63
+ }
64
+ cleanedTokens.push(token);
65
+ }
66
+
67
+ return {
68
+ cleanedArgs: consumed ? cleanedTokens.join(" ") : input.args,
69
+ activation,
70
+ activeSubskillsToPersist: activation
71
+ ? bindings
72
+ .filter(
73
+ binding => binding.plugin === activation.plugin && binding.activationArg === activation.activationArg,
74
+ )
75
+ .map(binding => ({
76
+ activationArg: binding.activationArg,
77
+ plugin: binding.plugin,
78
+ subskillName: binding.subskillName,
79
+ parent: binding.parent,
80
+ bindsTo: binding.bindsTo,
81
+ phase: binding.phase,
82
+ filePath: binding.filePath,
83
+ toolPaths: binding.toolPaths,
84
+ }))
85
+ : [],
86
+ };
87
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./activation";
2
+ export * from "./injection";
3
+ export * from "./loader";
4
+ export * from "./paths";
5
+ export * from "./schema";
6
+ export * from "./state";
7
+ export * from "./tools";
8
+ export * from "./types";
9
+ export * from "./validation";
@@ -0,0 +1,114 @@
1
+ import { readVisibleSkillActiveState } from "../../skill-state/active-state";
2
+ import { initialPhaseForSkill } from "../../skill-state/initial-phase";
3
+ import { readActiveSubskillsForParent } from "./state";
4
+ import { GJC_SUBSKILL_PARENT_AGENTS, type LoadedSubskillActivation } from "./types";
5
+
6
+ export async function readSubskillBody(filePath: string): Promise<string> {
7
+ const content = await Bun.file(filePath).text();
8
+ return content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
9
+ }
10
+
11
+ function escapeAttribute(value: string): string {
12
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
+ }
14
+
15
+ export function wrapSubskillBlock(
16
+ activation: {
17
+ plugin: string;
18
+ subskillName: string;
19
+ parent: string;
20
+ phase: string;
21
+ activationArg: string;
22
+ filePath: string;
23
+ },
24
+ body: string,
25
+ ): string {
26
+ return `\n\n---\n\n<gjc-subskill plugin="${escapeAttribute(activation.plugin)}" name="${escapeAttribute(activation.subskillName)}" parent="${escapeAttribute(activation.parent)}" phase="${escapeAttribute(activation.phase)}" arg="${escapeAttribute(activation.activationArg)}">\n${body}\n</gjc-subskill>`;
27
+ }
28
+
29
+ export async function resolveCurrentPhaseForParent(input: {
30
+ cwd: string;
31
+ sessionId?: string;
32
+ parent: string;
33
+ explicitPhase?: string;
34
+ }): Promise<string> {
35
+ const explicitPhase = input.explicitPhase?.trim();
36
+ if (explicitPhase) return explicitPhase;
37
+
38
+ const state = await readVisibleSkillActiveState(input.cwd, input.sessionId);
39
+ const persistedPhase = state?.active_skills?.find(entry => entry.skill === input.parent)?.phase?.trim();
40
+ if (persistedPhase) return persistedPhase;
41
+
42
+ if (state?.skill === input.parent) {
43
+ const statePhase = state.phase?.trim();
44
+ if (statePhase) return statePhase;
45
+ }
46
+
47
+ return initialPhaseForSkill(input.parent);
48
+ }
49
+
50
+ export async function buildSubskillInjection(input: {
51
+ cwd: string;
52
+ sessionId?: string;
53
+ skillName: string;
54
+ activation?: LoadedSubskillActivation;
55
+ currentPhase?: string;
56
+ }): Promise<{ block: string; details?: LoadedSubskillActivation } | null> {
57
+ const resolvedPhase = await resolveCurrentPhaseForParent({
58
+ cwd: input.cwd,
59
+ sessionId: input.sessionId,
60
+ parent: input.skillName,
61
+ explicitPhase: input.currentPhase,
62
+ });
63
+
64
+ const directActivation = input.activation;
65
+ if (directActivation?.parent === input.skillName && directActivation.phase === resolvedPhase) {
66
+ const body = await readSubskillBody(directActivation.filePath);
67
+ return { block: wrapSubskillBlock(directActivation, body), details: directActivation };
68
+ }
69
+
70
+ const [entry] = await readActiveSubskillsForParent({
71
+ cwd: input.cwd,
72
+ sessionId: input.sessionId,
73
+ parent: input.skillName,
74
+ phase: resolvedPhase,
75
+ });
76
+ if (!entry) return null;
77
+
78
+ const activation: LoadedSubskillActivation = {
79
+ plugin: entry.plugin,
80
+ subskillName: entry.subskillName,
81
+ parent: entry.parent,
82
+ bindsTo: entry.bindsTo,
83
+ phase: entry.phase,
84
+ activationArg: entry.activationArg,
85
+ filePath: entry.filePath,
86
+ toolPaths: entry.toolPaths,
87
+ };
88
+ const body = await readSubskillBody(activation.filePath);
89
+ return { block: wrapSubskillBlock(activation, body), details: activation };
90
+ }
91
+
92
+ export async function buildAgentSubskillInjection(input: {
93
+ cwd: string;
94
+ sessionId?: string;
95
+ agentName: string;
96
+ }): Promise<string> {
97
+ if (!(GJC_SUBSKILL_PARENT_AGENTS as readonly string[]).includes(input.agentName)) return "";
98
+
99
+ const entries = await readActiveSubskillsForParent({
100
+ cwd: input.cwd,
101
+ sessionId: input.sessionId,
102
+ parent: input.agentName,
103
+ phase: "prompt",
104
+ });
105
+ if (entries.length === 0) return "";
106
+
107
+ const blocks = await Promise.all(
108
+ entries.map(async entry => {
109
+ const body = await readSubskillBody(entry.filePath);
110
+ return wrapSubskillBlock(entry, body);
111
+ }),
112
+ );
113
+ return blocks.join("");
114
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { parseFrontmatter } from "@gajae-code/utils";
4
+ import { resolveWithinRoot } from "./paths";
5
+ import { parseManifest, parseSubskillFrontmatter } from "./schema";
6
+ import {
7
+ GJC_PLUGIN_MANIFEST_FILENAME,
8
+ GjcPluginLoadError,
9
+ type LoadedGjcPlugin,
10
+ type LoadedSubskillBinding,
11
+ type PhaseScopedToolBinding,
12
+ } from "./types";
13
+ import { buildParentArgMap, buildParentPhaseSet, validateBinding } from "./validation";
14
+
15
+ async function readJsonFile(filePath: string): Promise<unknown> {
16
+ try {
17
+ return JSON.parse(await fs.readFile(filePath, "utf8")) as unknown;
18
+ } catch (error) {
19
+ if (error instanceof SyntaxError) {
20
+ throw new GjcPluginLoadError("invalid_manifest", `Invalid GJC plugin manifest JSON at ${filePath}`, {
21
+ cause: error,
22
+ });
23
+ }
24
+ throw new GjcPluginLoadError("missing_file", `Missing GJC plugin manifest at ${filePath}`, {
25
+ cause: error instanceof Error ? error : undefined,
26
+ });
27
+ }
28
+ }
29
+
30
+ async function readRequiredText(filePath: string, kind: "sub-skill" | "tool"): Promise<string> {
31
+ try {
32
+ return await fs.readFile(filePath, "utf8");
33
+ } catch (error) {
34
+ throw new GjcPluginLoadError("missing_file", `Missing GJC plugin ${kind} file at ${filePath}`, {
35
+ cause: error instanceof Error ? error : undefined,
36
+ });
37
+ }
38
+ }
39
+
40
+ function parseFrontmatterToolPaths(fm: Record<string, unknown>): string[] {
41
+ const raw = fm.tools;
42
+ if (raw === undefined) return [];
43
+ if (typeof raw === "string") return raw.trim() ? [raw] : [];
44
+ if (Array.isArray(raw) && raw.every(item => typeof item === "string")) return [...raw];
45
+ return [];
46
+ }
47
+
48
+ function pushToolBinding(
49
+ toolBindings: PhaseScopedToolBinding[],
50
+ plugin: string,
51
+ parent: string,
52
+ phase: string,
53
+ toolPath: string,
54
+ ): void {
55
+ toolBindings.push({ plugin, parent, phase, toolPath });
56
+ }
57
+
58
+ export async function loadGjcPlugin(root: string): Promise<LoadedGjcPlugin> {
59
+ const pluginRoot = path.resolve(root);
60
+ const manifestPath = path.join(pluginRoot, GJC_PLUGIN_MANIFEST_FILENAME);
61
+ const manifest = parseManifest(await readJsonFile(manifestPath), manifestPath);
62
+ const manifestToolPaths = manifest.tools.map(rel => resolveWithinRoot(pluginRoot, rel));
63
+
64
+ for (const toolPath of manifestToolPaths) {
65
+ await readRequiredText(toolPath, "tool");
66
+ }
67
+
68
+ const bindings: LoadedSubskillBinding[] = [];
69
+ const toolBindings: PhaseScopedToolBinding[] = [];
70
+
71
+ for (const rel of manifest.subskills) {
72
+ const filePath = resolveWithinRoot(pluginRoot, rel);
73
+ const content = await readRequiredText(filePath, "sub-skill");
74
+ let parsed: { frontmatter: Record<string, unknown>; body: string };
75
+ try {
76
+ parsed = parseFrontmatter(content, { source: filePath, level: "fatal" });
77
+ } catch (error) {
78
+ throw new GjcPluginLoadError("invalid_frontmatter", `Invalid GJC sub-skill frontmatter at ${filePath}`, {
79
+ cause: error instanceof Error ? error : undefined,
80
+ });
81
+ }
82
+ const frontmatter = parseSubskillFrontmatter(parsed.frontmatter, filePath);
83
+ validateBinding(frontmatter);
84
+ const frontmatterToolPaths = parseFrontmatterToolPaths(parsed.frontmatter).map(toolRel =>
85
+ resolveWithinRoot(pluginRoot, toolRel),
86
+ );
87
+ for (const toolPath of frontmatterToolPaths) {
88
+ await readRequiredText(toolPath, "tool");
89
+ }
90
+ const toolPaths = [...manifestToolPaths, ...frontmatterToolPaths];
91
+ const binding: LoadedSubskillBinding = {
92
+ plugin: manifest.name,
93
+ subskillName: frontmatter.name,
94
+ parent: frontmatter.binds_to,
95
+ bindsTo: frontmatter.binds_to,
96
+ phase: frontmatter.phase,
97
+ activationArg: frontmatter.activation_arg,
98
+ description: frontmatter.description,
99
+ filePath,
100
+ body: parsed.body,
101
+ toolPaths,
102
+ };
103
+ bindings.push(binding);
104
+ for (const toolPath of toolPaths) {
105
+ pushToolBinding(toolBindings, manifest.name, binding.parent, binding.phase, toolPath);
106
+ }
107
+ }
108
+
109
+ buildParentArgMap(bindings);
110
+ buildParentPhaseSet(bindings);
111
+
112
+ return {
113
+ name: manifest.name,
114
+ version: manifest.version,
115
+ root: pluginRoot,
116
+ manifestPath,
117
+ bindings,
118
+ toolBindings,
119
+ };
120
+ }
121
+
122
+ export async function loadGjcPlugins(roots: readonly string[]): Promise<LoadedGjcPlugin[]> {
123
+ const plugins: LoadedGjcPlugin[] = [];
124
+ for (const root of roots) {
125
+ plugins.push(await loadGjcPlugin(root));
126
+ }
127
+ const bindings = plugins.flatMap(plugin => plugin.bindings);
128
+ buildParentArgMap(bindings);
129
+ buildParentPhaseSet(bindings);
130
+ return plugins;
131
+ }
@@ -0,0 +1,66 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getAgentDir, pathIsWithin } from "@gajae-code/utils";
4
+ import { GJC_PLUGIN_MANIFEST_FILENAME, GjcPluginLoadError } from "./types";
5
+
6
+ export function gjcPluginUserRoot(): string {
7
+ return path.join(getAgentDir(), "gjc-plugins");
8
+ }
9
+
10
+ export function gjcPluginProjectRoot(cwd: string): string {
11
+ return path.join(cwd, ".gjc", "gjc-plugins");
12
+ }
13
+
14
+ function isEnoent(error: unknown): boolean {
15
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
16
+ }
17
+
18
+ export async function rootContainsGjcManifest(dir: string): Promise<boolean> {
19
+ try {
20
+ await fs.access(path.join(dir, GJC_PLUGIN_MANIFEST_FILENAME));
21
+ return true;
22
+ } catch (error) {
23
+ if (isEnoent(error)) return false;
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ async function discoverGjcPluginRootsIn(baseDir: string): Promise<string[]> {
29
+ if (await rootContainsGjcManifest(baseDir)) return [baseDir];
30
+
31
+ let entries: import("node:fs").Dirent[];
32
+ try {
33
+ entries = await fs.readdir(baseDir, { withFileTypes: true });
34
+ } catch (error) {
35
+ if (isEnoent(error)) return [];
36
+ throw error;
37
+ }
38
+
39
+ const roots = await Promise.all(
40
+ entries
41
+ .filter(entry => entry.isDirectory() || entry.isSymbolicLink())
42
+ .map(async entry => {
43
+ const dir = path.join(baseDir, entry.name);
44
+ return (await rootContainsGjcManifest(dir)) ? dir : null;
45
+ }),
46
+ );
47
+
48
+ return roots.filter((root): root is string => root !== null);
49
+ }
50
+
51
+ export async function discoverGjcPluginRoots({ cwd }: { cwd: string; home?: string }): Promise<string[]> {
52
+ const roots = await Promise.all([
53
+ discoverGjcPluginRootsIn(gjcPluginUserRoot()),
54
+ discoverGjcPluginRootsIn(gjcPluginProjectRoot(cwd)),
55
+ ]);
56
+ return roots.flat();
57
+ }
58
+
59
+ export function resolveWithinRoot(root: string, rel: string): string {
60
+ const resolvedRoot = path.resolve(root);
61
+ const resolvedPath = path.resolve(resolvedRoot, rel);
62
+ if (!pathIsWithin(resolvedRoot, resolvedPath)) {
63
+ throw new GjcPluginLoadError("missing_file", `GJC plugin path escapes root: ${rel}`);
64
+ }
65
+ return resolvedPath;
66
+ }