@gajae-code/coding-agent 0.7.2 → 0.7.4

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 (154) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/bin/gjc.js +4 -0
  3. package/dist/types/cli/mcp-cli.d.ts +25 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  12. package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
  13. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
  14. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
  17. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
  18. package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
  19. package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
  20. package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
  21. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
  22. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
  23. package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
  24. package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
  25. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  26. package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
  28. package/dist/types/gjc-runtime/tmux-common.d.ts +20 -1
  29. package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  34. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  35. package/dist/types/notifications/html-format.d.ts +11 -0
  36. package/dist/types/notifications/index.d.ts +149 -1
  37. package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
  38. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  39. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  40. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  41. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  42. package/dist/types/notifications/recent-activity.d.ts +35 -0
  43. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  44. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  45. package/dist/types/notifications/topic-registry.d.ts +12 -9
  46. package/dist/types/runtime-mcp/types.d.ts +7 -0
  47. package/dist/types/sdk.d.ts +2 -0
  48. package/dist/types/session/agent-session.d.ts +14 -4
  49. package/dist/types/session/blob-store.d.ts +25 -0
  50. package/dist/types/session/session-manager.d.ts +57 -0
  51. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  52. package/dist/types/system-prompt.d.ts +2 -0
  53. package/dist/types/task/executor.d.ts +9 -1
  54. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  55. package/dist/types/tools/index.d.ts +3 -1
  56. package/dist/types/utils/changelog.d.ts +1 -0
  57. package/dist/types/web/insane/url-guard.d.ts +6 -3
  58. package/dist/types/web/scrapers/types.d.ts +5 -0
  59. package/dist/types/web/scrapers/utils.d.ts +7 -1
  60. package/package.json +11 -9
  61. package/scripts/g004-tmux-smoke.ts +100 -0
  62. package/scripts/g005-daemon-smoke.ts +181 -0
  63. package/scripts/g011-daemon-path-smoke.ts +153 -0
  64. package/src/cli/mcp-cli.ts +272 -0
  65. package/src/cli/plugin-cli.ts +66 -3
  66. package/src/cli.ts +27 -6
  67. package/src/commands/mcp.ts +117 -0
  68. package/src/commands/plugin.ts +4 -0
  69. package/src/commands/session.ts +18 -0
  70. package/src/config/keybindings.ts +2 -2
  71. package/src/config/model-profile-activation.ts +55 -7
  72. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +7 -6
  75. package/src/defaults/gjc/skills/team/SKILL.md +5 -3
  76. package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
  77. package/src/export/html/index.ts +2 -2
  78. package/src/extensibility/extensions/runner.ts +1 -0
  79. package/src/extensibility/gjc-plugins/compiler.ts +351 -0
  80. package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
  81. package/src/extensibility/gjc-plugins/index.ts +9 -0
  82. package/src/extensibility/gjc-plugins/injection.ts +109 -0
  83. package/src/extensibility/gjc-plugins/installer.ts +434 -0
  84. package/src/extensibility/gjc-plugins/loader.ts +3 -1
  85. package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
  86. package/src/extensibility/gjc-plugins/observability.ts +84 -0
  87. package/src/extensibility/gjc-plugins/paths.ts +1 -1
  88. package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
  89. package/src/extensibility/gjc-plugins/registry.ts +180 -0
  90. package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
  91. package/src/extensibility/gjc-plugins/schema.ts +250 -20
  92. package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
  93. package/src/extensibility/gjc-plugins/types.ts +199 -3
  94. package/src/extensibility/gjc-plugins/validation.ts +80 -0
  95. package/src/extensibility/skills.ts +15 -0
  96. package/src/gjc-runtime/launch-tmux.ts +61 -7
  97. package/src/gjc-runtime/psmux-detect.ts +239 -0
  98. package/src/gjc-runtime/team-runtime.ts +56 -23
  99. package/src/gjc-runtime/tmux-common.ts +30 -3
  100. package/src/gjc-runtime/tmux-sessions.ts +51 -1
  101. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  102. package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
  103. package/src/hooks/skill-state.ts +57 -0
  104. package/src/internal-urls/docs-index.generated.ts +12 -8
  105. package/src/main.ts +14 -3
  106. package/src/modes/bridge/bridge-mode.ts +11 -0
  107. package/src/modes/components/custom-editor.ts +2 -0
  108. package/src/modes/components/footer.ts +2 -3
  109. package/src/modes/components/hook-editor.ts +1 -1
  110. package/src/modes/components/hook-selector.ts +67 -43
  111. package/src/modes/components/model-selector.ts +56 -11
  112. package/src/modes/components/status-line/git-utils.ts +25 -0
  113. package/src/modes/components/status-line.ts +10 -11
  114. package/src/modes/components/welcome.ts +2 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  116. package/src/modes/controllers/selector-controller.ts +53 -11
  117. package/src/modes/interactive-mode.ts +4 -1
  118. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  119. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  120. package/src/modes/theme/defaults/index.ts +2 -0
  121. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  122. package/src/notifications/html-format.ts +38 -0
  123. package/src/notifications/index.ts +242 -12
  124. package/src/notifications/lifecycle-commands.ts +228 -0
  125. package/src/notifications/lifecycle-control-runtime.ts +400 -0
  126. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  127. package/src/notifications/operator-runtime.ts +171 -0
  128. package/src/notifications/rate-limit-pool.ts +19 -0
  129. package/src/notifications/recent-activity.ts +132 -0
  130. package/src/notifications/telegram-daemon.ts +778 -257
  131. package/src/notifications/telegram-reference.ts +25 -7
  132. package/src/notifications/topic-registry.ts +23 -9
  133. package/src/prompts/agents/executor.md +2 -2
  134. package/src/runtime-mcp/transports/stdio.ts +38 -4
  135. package/src/runtime-mcp/types.ts +7 -0
  136. package/src/sdk.ts +157 -10
  137. package/src/session/agent-session.ts +166 -74
  138. package/src/session/blob-store.ts +196 -8
  139. package/src/session/session-manager.ts +678 -7
  140. package/src/slash-commands/builtin-registry.ts +23 -3
  141. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  142. package/src/slash-commands/helpers/parse.ts +2 -1
  143. package/src/system-prompt.ts +9 -0
  144. package/src/task/executor.ts +31 -7
  145. package/src/task/index.ts +2 -0
  146. package/src/tools/ask.ts +5 -1
  147. package/src/tools/bash.ts +9 -0
  148. package/src/tools/composer-bash-policy.ts +96 -0
  149. package/src/tools/fetch.ts +18 -2
  150. package/src/tools/index.ts +3 -1
  151. package/src/utils/changelog.ts +8 -0
  152. package/src/web/insane/url-guard.ts +18 -14
  153. package/src/web/scrapers/types.ts +143 -45
  154. package/src/web/scrapers/utils.ts +70 -19
@@ -1,6 +1,43 @@
1
- import { GJC_PLUGIN_KIND, GjcPluginLoadError, type GjcPluginManifest, type SubskillFrontmatter } from "./types";
1
+ import {
2
+ GJC_PLUGIN_KIND,
3
+ GJC_SUBSKILL_PARENT_AGENTS,
4
+ type GjcPluginAgentAppendixManifestEntry,
5
+ type GjcPluginAppendixManifestEntry,
6
+ type GjcPluginHookManifestEntry,
7
+ GjcPluginLoadError,
8
+ type GjcPluginManifest,
9
+ type GjcPluginMcpManifestEntry,
10
+ type GjcPluginMcpTransport,
11
+ type GjcPluginToolManifestEntry,
12
+ type GjcSubskillParentAgent,
13
+ type SubskillFrontmatter,
14
+ } from "./types";
2
15
 
3
- const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "hooks", "mcp", "mcpServers", "agents"];
16
+ /**
17
+ * Top-level surfaces that may never appear in a GJC plugin bundle: bundles may
18
+ * only EXTEND existing skills/agents, never register new top-level ones.
19
+ */
20
+ const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "agents"];
21
+
22
+ /**
23
+ * Ambiguous legacy aliases. `mcps` is the only canonical MCP key; these are
24
+ * rejected as `unsupported_surface` to avoid accidental legacy shape ambiguity.
25
+ */
26
+ const UNSUPPORTED_ALIAS_KEYS = ["mcp", "mcpServers"];
27
+
28
+ const KNOWN_MANIFEST_KEYS = new Set([
29
+ "kind",
30
+ "name",
31
+ "version",
32
+ "subskills",
33
+ "tools",
34
+ "hooks",
35
+ "mcps",
36
+ "system_appendix",
37
+ "agent-appendix",
38
+ ]);
39
+
40
+ const MCP_TRANSPORTS: readonly GjcPluginMcpTransport[] = ["stdio", "http", "sse"];
4
41
 
5
42
  function isRecord(value: unknown): value is Record<string, unknown> {
6
43
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -16,14 +53,194 @@ function requireNonEmptyString(value: unknown, field: string, filePath: string):
16
53
  return value;
17
54
  }
18
55
 
19
- function requireStringArray(value: unknown, field: string, manifestPath: string): string[] {
56
+ function manifestString(value: unknown, field: string, manifestPath: string): string {
57
+ if (typeof value !== "string" || value.trim().length === 0) {
58
+ throw new GjcPluginLoadError(
59
+ "invalid_manifest",
60
+ `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be a non-empty string`,
61
+ );
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function optionalStringArray(value: unknown, field: string, manifestPath: string): string[] {
67
+ if (value === undefined) return [];
20
68
  if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
21
69
  throw new GjcPluginLoadError(
22
70
  "invalid_manifest",
23
71
  `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be a string array`,
24
72
  );
25
73
  }
26
- return [...value];
74
+ return [...(value as string[])];
75
+ }
76
+
77
+ function optionalArray(value: unknown, field: string, manifestPath: string): unknown[] {
78
+ if (value === undefined) return [];
79
+ if (!Array.isArray(value)) {
80
+ throw new GjcPluginLoadError(
81
+ "invalid_manifest",
82
+ `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an array`,
83
+ );
84
+ }
85
+ return value;
86
+ }
87
+
88
+ function deriveToolName(toolPath: string): string {
89
+ const base = toolPath.split("/").pop() ?? toolPath;
90
+ return base.replace(/\.[^.]+$/, "");
91
+ }
92
+
93
+ function parseTools(value: unknown, manifestPath: string): GjcPluginToolManifestEntry[] {
94
+ const raw = optionalArray(value, "tools", manifestPath);
95
+ return raw.map((entry, index) => {
96
+ // Legacy string shorthand: subskill-scoped tool path only.
97
+ if (typeof entry === "string") {
98
+ if (entry.trim().length === 0) {
99
+ throw new GjcPluginLoadError(
100
+ "invalid_manifest",
101
+ `Invalid GJC plugin manifest at ${manifestPath}: tools[${index}] must be a non-empty path`,
102
+ );
103
+ }
104
+ return { name: deriveToolName(entry), path: entry, surface: "subskill" };
105
+ }
106
+ if (!isRecord(entry)) {
107
+ throw new GjcPluginLoadError(
108
+ "invalid_manifest",
109
+ `Invalid GJC plugin manifest at ${manifestPath}: tools[${index}] must be a string or object`,
110
+ );
111
+ }
112
+ const name = manifestString(entry.name, `tools[${index}].name`, manifestPath);
113
+ const path = manifestString(entry.path, `tools[${index}].path`, manifestPath);
114
+ const description =
115
+ entry.description === undefined
116
+ ? undefined
117
+ : manifestString(entry.description, `tools[${index}].description`, manifestPath);
118
+ const sha256 =
119
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `tools[${index}].sha256`, manifestPath);
120
+ return { name, path, description, sha256, surface: "always-on" };
121
+ });
122
+ }
123
+
124
+ function parseHooks(value: unknown, manifestPath: string): GjcPluginHookManifestEntry[] {
125
+ const raw = optionalArray(value, "hooks", manifestPath);
126
+ return raw.map((entry, index) => {
127
+ if (!isRecord(entry)) {
128
+ throw new GjcPluginLoadError(
129
+ "invalid_manifest",
130
+ `Invalid GJC plugin manifest at ${manifestPath}: hooks[${index}] must be an object`,
131
+ );
132
+ }
133
+ const name = manifestString(entry.name, `hooks[${index}].name`, manifestPath);
134
+ const event = manifestString(entry.event, `hooks[${index}].event`, manifestPath);
135
+ const path = manifestString(entry.path, `hooks[${index}].path`, manifestPath);
136
+ const target =
137
+ entry.target === undefined ? undefined : manifestString(entry.target, `hooks[${index}].target`, manifestPath);
138
+ let phase: "before" | "after" | undefined;
139
+ if (entry.phase !== undefined) {
140
+ if (entry.phase !== "before" && entry.phase !== "after") {
141
+ throw new GjcPluginLoadError(
142
+ "invalid_manifest",
143
+ `Invalid GJC plugin manifest at ${manifestPath}: hooks[${index}].phase must be "before" or "after"`,
144
+ );
145
+ }
146
+ phase = entry.phase;
147
+ }
148
+ const sha256 =
149
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `hooks[${index}].sha256`, manifestPath);
150
+ return { name, event, target, phase, path, sha256 };
151
+ });
152
+ }
153
+
154
+ function parseMcps(value: unknown, manifestPath: string): GjcPluginMcpManifestEntry[] {
155
+ const raw = optionalArray(value, "mcps", manifestPath);
156
+ return raw.map((entry, index) => {
157
+ if (!isRecord(entry)) {
158
+ throw new GjcPluginLoadError(
159
+ "invalid_manifest",
160
+ `Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}] must be an object`,
161
+ );
162
+ }
163
+ const name = manifestString(entry.name, `mcps[${index}].name`, manifestPath);
164
+ const transport = entry.transport;
165
+ if (typeof transport !== "string" || !MCP_TRANSPORTS.includes(transport as GjcPluginMcpTransport)) {
166
+ throw new GjcPluginLoadError(
167
+ "invalid_manifest",
168
+ `Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}].transport must be one of ${MCP_TRANSPORTS.join(", ")}`,
169
+ );
170
+ }
171
+ const command =
172
+ entry.command === undefined
173
+ ? undefined
174
+ : manifestString(entry.command, `mcps[${index}].command`, manifestPath);
175
+ const url = entry.url === undefined ? undefined : manifestString(entry.url, `mcps[${index}].url`, manifestPath);
176
+ const cwd = entry.cwd === undefined ? undefined : manifestString(entry.cwd, `mcps[${index}].cwd`, manifestPath);
177
+ let args: string[] | undefined;
178
+ if (entry.args !== undefined) {
179
+ if (!Array.isArray(entry.args) || !entry.args.every(item => typeof item === "string")) {
180
+ throw new GjcPluginLoadError(
181
+ "invalid_manifest",
182
+ `Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}].args must be a string array`,
183
+ );
184
+ }
185
+ args = [...(entry.args as string[])];
186
+ }
187
+ let headers: Record<string, string> | undefined;
188
+ if (entry.headers !== undefined) {
189
+ if (!isRecord(entry.headers) || !Object.values(entry.headers).every(v => typeof v === "string")) {
190
+ throw new GjcPluginLoadError(
191
+ "invalid_manifest",
192
+ `Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}].headers must be a string map`,
193
+ );
194
+ }
195
+ headers = { ...(entry.headers as Record<string, string>) };
196
+ }
197
+ const sha256 =
198
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `mcps[${index}].sha256`, manifestPath);
199
+ return { name, transport: transport as GjcPluginMcpTransport, command, args, cwd, url, headers, sha256 };
200
+ });
201
+ }
202
+
203
+ function parseAppendixEntry(entry: unknown, field: string, manifestPath: string): GjcPluginAppendixManifestEntry {
204
+ if (!isRecord(entry)) {
205
+ throw new GjcPluginLoadError(
206
+ "invalid_manifest",
207
+ `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an object`,
208
+ );
209
+ }
210
+ const name = manifestString(entry.name, `${field}.name`, manifestPath);
211
+ const path = entry.path === undefined ? undefined : manifestString(entry.path, `${field}.path`, manifestPath);
212
+ // Content may be empty/whitespace here; the compiler enforces non-empty and
213
+ // maps emptiness to invalid_appendix (not invalid_manifest).
214
+ if (entry.content !== undefined && typeof entry.content !== "string") {
215
+ throw new GjcPluginLoadError(
216
+ "invalid_manifest",
217
+ `Invalid GJC plugin manifest at ${manifestPath}: ${field}.content must be a string`,
218
+ );
219
+ }
220
+ const content = entry.content as string | undefined;
221
+ const sha256 =
222
+ entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `${field}.sha256`, manifestPath);
223
+ return { name, path, content, sha256 };
224
+ }
225
+
226
+ function parseSystemAppendix(value: unknown, manifestPath: string): GjcPluginAppendixManifestEntry[] {
227
+ const raw = optionalArray(value, "system_appendix", manifestPath);
228
+ return raw.map((entry, index) => parseAppendixEntry(entry, `system_appendix[${index}]`, manifestPath));
229
+ }
230
+
231
+ function parseAgentAppendix(value: unknown, manifestPath: string): GjcPluginAgentAppendixManifestEntry[] {
232
+ const raw = optionalArray(value, "agent-appendix", manifestPath);
233
+ return raw.map((entry, index) => {
234
+ const base = parseAppendixEntry(entry, `agent-appendix[${index}]`, manifestPath);
235
+ const agent = (entry as Record<string, unknown>).agent;
236
+ if (typeof agent !== "string" || !GJC_SUBSKILL_PARENT_AGENTS.includes(agent as GjcSubskillParentAgent)) {
237
+ throw new GjcPluginLoadError(
238
+ "invalid_parent",
239
+ `Invalid GJC plugin manifest at ${manifestPath}: agent-appendix[${index}].agent must be one of ${GJC_SUBSKILL_PARENT_AGENTS.join(", ")}`,
240
+ );
241
+ }
242
+ return { ...base, agent: agent as GjcSubskillParentAgent };
243
+ });
27
244
  }
28
245
 
29
246
  export function parseManifest(raw: unknown, manifestPath: string): GjcPluginManifest {
@@ -40,31 +257,44 @@ export function parseManifest(raw: unknown, manifestPath: string): GjcPluginMani
40
257
  }
41
258
  }
42
259
 
260
+ for (const key of UNSUPPORTED_ALIAS_KEYS) {
261
+ if (Object.hasOwn(raw, key)) {
262
+ throw new GjcPluginLoadError(
263
+ "unsupported_surface",
264
+ `Unsupported GJC plugin surface in ${manifestPath}: ${key} (use the canonical "mcps" key)`,
265
+ );
266
+ }
267
+ }
268
+
269
+ for (const key of Object.keys(raw)) {
270
+ if (!KNOWN_MANIFEST_KEYS.has(key)) {
271
+ throw new GjcPluginLoadError(
272
+ "unsupported_surface",
273
+ `Unsupported GJC plugin surface in ${manifestPath}: ${key}`,
274
+ );
275
+ }
276
+ }
277
+
43
278
  if (raw.kind !== GJC_PLUGIN_KIND) {
44
279
  throw new GjcPluginLoadError(
45
280
  "invalid_kind",
46
281
  `Invalid GJC plugin kind in ${manifestPath}: expected ${GJC_PLUGIN_KIND}`,
47
282
  );
48
283
  }
49
- if (typeof raw.name !== "string" || raw.name.trim().length === 0) {
50
- throw new GjcPluginLoadError(
51
- "invalid_manifest",
52
- `Invalid GJC plugin manifest at ${manifestPath}: name must be a non-empty string`,
53
- );
54
- }
55
- if (typeof raw.version !== "string" || raw.version.trim().length === 0) {
56
- throw new GjcPluginLoadError(
57
- "invalid_manifest",
58
- `Invalid GJC plugin manifest at ${manifestPath}: version must be a non-empty string`,
59
- );
60
- }
284
+
285
+ const name = manifestString(raw.name, "name", manifestPath);
286
+ const version = manifestString(raw.version, "version", manifestPath);
61
287
 
62
288
  return {
63
- name: raw.name,
64
- version: raw.version,
289
+ name,
290
+ version,
65
291
  kind: GJC_PLUGIN_KIND,
66
- subskills: requireStringArray(raw.subskills, "subskills", manifestPath),
67
- tools: requireStringArray(raw.tools, "tools", manifestPath),
292
+ subskills: optionalStringArray(raw.subskills, "subskills", manifestPath),
293
+ tools: parseTools(raw.tools, manifestPath),
294
+ hooks: parseHooks(raw.hooks, manifestPath),
295
+ mcps: parseMcps(raw.mcps, manifestPath),
296
+ systemAppendix: parseSystemAppendix(raw.system_appendix, manifestPath),
297
+ agentAppendix: parseAgentAppendix(raw["agent-appendix"], manifestPath),
68
298
  };
69
299
  }
70
300
 
@@ -0,0 +1,147 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import type { GjcPluginLoadErrorCode, GjcPluginRegistryEntry } from "./types";
5
+
6
+ /**
7
+ * Session-start validation: the registry is the collision authority. Capability
8
+ * provider output is supplied as EVIDENCE only; plugin surfaces are never
9
+ * resolved by capability first-wins. Offending surfaces are quarantined
10
+ * fail-closed with a stable error code rather than silently shadowed.
11
+ */
12
+
13
+ export interface SessionCapabilityEvidence {
14
+ /** Built-in + provider tool names already present before plugin insertion. */
15
+ toolNames?: Iterable<string>;
16
+ /** Non-plugin MCP server names from providers/built-ins. */
17
+ mcpNames?: Iterable<string>;
18
+ /** Non-plugin hook keys (event:phase:target:name) from providers/built-ins. */
19
+ hookKeys?: Iterable<string>;
20
+ /** Existing appendix extension ids already present. */
21
+ appendixIds?: Iterable<string>;
22
+ }
23
+
24
+ export interface SessionQuarantine {
25
+ plugin: string;
26
+ surfaceId: string;
27
+ code: GjcPluginLoadErrorCode;
28
+ message: string;
29
+ }
30
+
31
+ export interface SessionValidationResult {
32
+ /** Registry entries (enabled, non-quarantined) whose surfaces may activate. */
33
+ active: GjcPluginRegistryEntry[];
34
+ /** Per-surface quarantine records (fail-closed). */
35
+ quarantine: SessionQuarantine[];
36
+ }
37
+
38
+ function sha256(buf: Buffer): string {
39
+ return createHash("sha256").update(buf).digest("hex");
40
+ }
41
+
42
+ /**
43
+ * Re-verify that the installed files still match the registry's recorded hashes.
44
+ * Drift (manual edits, partial writes) quarantines the whole plugin with
45
+ * runtime_mismatch.
46
+ */
47
+ export async function verifyEntryHashes(entry: GjcPluginRegistryEntry): Promise<SessionQuarantine | null> {
48
+ for (const file of entry.copiedFiles) {
49
+ const abs = path.join(entry.pluginRoot, file.relativePath);
50
+ let buf: Buffer;
51
+ try {
52
+ buf = await fs.readFile(abs);
53
+ } catch {
54
+ return {
55
+ plugin: entry.name,
56
+ surfaceId: `plugin:${entry.name}`,
57
+ code: "runtime_mismatch",
58
+ message: `Installed file missing: ${file.relativePath}`,
59
+ };
60
+ }
61
+ if (sha256(buf) !== file.sha256) {
62
+ return {
63
+ plugin: entry.name,
64
+ surfaceId: `plugin:${entry.name}`,
65
+ code: "runtime_mismatch",
66
+ message: `Installed file hash drift: ${file.relativePath}`,
67
+ };
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function activeSurfaceIds(entry: GjcPluginRegistryEntry): {
74
+ tools: { id: string; name: string }[];
75
+ mcps: { id: string; name: string }[];
76
+ hooks: { id: string }[];
77
+ appendices: { id: string }[];
78
+ } {
79
+ const disabled = new Set(entry.disabledSurfaceIds);
80
+ return {
81
+ tools: entry.surfaces.tools
82
+ .filter(t => !disabled.has(t.extensionId))
83
+ .map(t => ({ id: t.extensionId, name: t.name })),
84
+ mcps: entry.surfaces.mcps
85
+ .filter(m => !disabled.has(m.extensionId))
86
+ .map(m => ({ id: m.extensionId, name: m.name })),
87
+ hooks: entry.surfaces.hooks.filter(h => !disabled.has(h.extensionId)).map(h => ({ id: h.extensionId })),
88
+ appendices: [...entry.surfaces.systemAppendices, ...entry.surfaces.agentAppendices]
89
+ .filter(a => !disabled.has(a.extensionId))
90
+ .map(a => ({ id: a.extensionId })),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Validate the effective installed registry against the current capability
96
+ * universe (evidence) and across plugins. Returns the entries that may activate
97
+ * plus per-surface quarantine records. Disabled entries/surfaces are skipped
98
+ * (not an error).
99
+ */
100
+ export function validateSessionBundles(
101
+ entries: readonly GjcPluginRegistryEntry[],
102
+ evidence: SessionCapabilityEvidence = {},
103
+ preQuarantined: readonly SessionQuarantine[] = [],
104
+ ): SessionValidationResult {
105
+ const quarantine: SessionQuarantine[] = [...preQuarantined];
106
+ const quarantinedPlugins = new Set(preQuarantined.map(q => q.plugin));
107
+
108
+ const seenTools = new Set<string>(evidence.toolNames ?? []);
109
+ const seenMcps = new Set<string>(evidence.mcpNames ?? []);
110
+ const seenHooks = new Set<string>(evidence.hookKeys ?? []);
111
+ const seenAppendices = new Set<string>(evidence.appendixIds ?? []);
112
+
113
+ const active: GjcPluginRegistryEntry[] = [];
114
+ for (const entry of entries) {
115
+ if (!entry.enabled) continue; // user-disabled, not an error
116
+ if (quarantinedPlugins.has(entry.name)) continue;
117
+ const surfaces = activeSurfaceIds(entry);
118
+ let collided = false;
119
+ const recordCollision = (surfaceId: string, what: string): void => {
120
+ collided = true;
121
+ quarantine.push({
122
+ plugin: entry.name,
123
+ surfaceId,
124
+ code: "session_collision",
125
+ message: `${what} collides with an existing capability/plugin; fail-closed (no shadowing)`,
126
+ });
127
+ };
128
+ for (const t of surfaces.tools) if (seenTools.has(t.name)) recordCollision(t.id, `tool "${t.name}"`);
129
+ for (const m of surfaces.mcps) if (seenMcps.has(m.name)) recordCollision(m.id, `mcp "${m.name}"`);
130
+ for (const h of surfaces.hooks) if (seenHooks.has(h.id)) recordCollision(h.id, `hook "${h.id}"`);
131
+ for (const a of surfaces.appendices) if (seenAppendices.has(a.id)) recordCollision(a.id, `appendix "${a.id}"`);
132
+
133
+ if (collided) {
134
+ // Fail-closed for the whole plugin entry so partial activation cannot
135
+ // leave a half-applied bundle.
136
+ continue;
137
+ }
138
+ // Reserve this plugin's surfaces so a later plugin colliding with it is
139
+ // also caught (deterministic order => earlier plugin wins reservation).
140
+ for (const t of surfaces.tools) seenTools.add(t.name);
141
+ for (const m of surfaces.mcps) seenMcps.add(m.name);
142
+ for (const h of surfaces.hooks) seenHooks.add(h.id);
143
+ for (const a of surfaces.appendices) seenAppendices.add(a.id);
144
+ active.push(entry);
145
+ }
146
+ return { active, quarantine };
147
+ }
@@ -19,12 +19,62 @@ export const GJC_AGENT_SUBSKILL_PHASES: Record<GjcSubskillParentAgent, string[]>
19
19
  critic: ["prompt"],
20
20
  };
21
21
 
22
+ export interface GjcPluginToolManifestEntry {
23
+ name: string;
24
+ path: string;
25
+ description?: string;
26
+ sha256?: string;
27
+ /**
28
+ * "always-on" object entries are activated for the whole session; legacy
29
+ * string shorthand stays "subskill"-scoped and is only attached to subskill
30
+ * bindings (never registered as an always-on tool surface).
31
+ */
32
+ surface: "subskill" | "always-on";
33
+ }
34
+
35
+ export interface GjcPluginHookManifestEntry {
36
+ name: string;
37
+ event: string;
38
+ target?: string;
39
+ phase?: "before" | "after";
40
+ path: string;
41
+ sha256?: string;
42
+ }
43
+
44
+ export type GjcPluginMcpTransport = "stdio" | "http" | "sse";
45
+
46
+ export interface GjcPluginMcpManifestEntry {
47
+ name: string;
48
+ transport: GjcPluginMcpTransport;
49
+ command?: string;
50
+ args?: string[];
51
+ cwd?: string;
52
+ url?: string;
53
+ headers?: Record<string, string>;
54
+ sha256?: string;
55
+ }
56
+
57
+ export interface GjcPluginAppendixManifestEntry {
58
+ name: string;
59
+ path?: string;
60
+ content?: string;
61
+ sha256?: string;
62
+ }
63
+
64
+ export interface GjcPluginAgentAppendixManifestEntry extends GjcPluginAppendixManifestEntry {
65
+ agent: GjcSubskillParentAgent;
66
+ }
67
+
22
68
  export interface GjcPluginManifest {
23
69
  name: string;
24
70
  version: string;
25
71
  kind: "gajae-code-plugin";
26
72
  subskills: string[];
27
- tools: string[];
73
+ tools: GjcPluginToolManifestEntry[];
74
+ hooks: GjcPluginHookManifestEntry[];
75
+ mcps: GjcPluginMcpManifestEntry[];
76
+ systemAppendix: GjcPluginAppendixManifestEntry[];
77
+ agentAppendix: GjcPluginAgentAppendixManifestEntry[];
28
78
  }
29
79
 
30
80
  export interface SubskillFrontmatter {
@@ -76,15 +126,33 @@ export interface LoadedGjcPlugin {
76
126
  }
77
127
 
78
128
  export type GjcPluginLoadErrorCode =
129
+ // Parse-time
79
130
  | "forbidden_surface"
80
131
  | "invalid_manifest"
132
+ | "invalid_kind"
133
+ | "unsupported_surface"
134
+ // Compile-time
81
135
  | "invalid_frontmatter"
82
136
  | "invalid_parent"
83
137
  | "invalid_phase"
138
+ | "missing_file"
139
+ | "hash_mismatch"
140
+ | "invalid_appendix"
141
+ | "invalid_hook"
142
+ | "invalid_mcp"
143
+ // Install-time
84
144
  | "duplicate_arg"
85
145
  | "duplicate_parent_phase"
86
- | "missing_file"
87
- | "invalid_kind";
146
+ | "duplicate_tool"
147
+ | "duplicate_hook"
148
+ | "duplicate_mcp"
149
+ | "duplicate_appendix"
150
+ | "security_policy"
151
+ | "install_conflict"
152
+ // Session-start / runtime
153
+ | "session_collision"
154
+ | "runtime_mismatch"
155
+ | "quarantined_surface";
88
156
 
89
157
  export class GjcPluginLoadError extends Error {
90
158
  readonly code: GjcPluginLoadErrorCode;
@@ -95,3 +163,131 @@ export class GjcPluginLoadError extends Error {
95
163
  this.code = code;
96
164
  }
97
165
  }
166
+
167
+ export type GjcPluginScope = "user" | "project";
168
+
169
+ export type GjcPluginSourceKind = "path" | "git" | "tarball";
170
+
171
+ export interface GjcPluginCopiedFile {
172
+ relativePath: string;
173
+ sha256: string;
174
+ bytes: number;
175
+ }
176
+
177
+ export interface NormalizedSubskillSurface {
178
+ extensionId: string;
179
+ name: string;
180
+ description: string;
181
+ parent: string;
182
+ phase: string;
183
+ activationArg: string;
184
+ relativePath: string;
185
+ sha256: string;
186
+ }
187
+
188
+ export interface NormalizedToolSurface {
189
+ extensionId: string;
190
+ name: string;
191
+ relativePath: string;
192
+ sha256: string;
193
+ description?: string;
194
+ }
195
+
196
+ export interface NormalizedHookSurface {
197
+ extensionId: string;
198
+ name: string;
199
+ event: string;
200
+ target?: string;
201
+ phase?: "before" | "after";
202
+ relativePath: string;
203
+ sha256: string;
204
+ }
205
+
206
+ export interface NormalizedMcpSurface {
207
+ extensionId: string;
208
+ name: string;
209
+ transport: GjcPluginMcpTransport;
210
+ configHash: string;
211
+ config: GjcPluginMcpManifestEntry;
212
+ }
213
+
214
+ export interface NormalizedAppendixSurface {
215
+ extensionId: string;
216
+ name: string;
217
+ relativePath?: string;
218
+ /** Inline appendix body (when the manifest used `content` instead of `path`). */
219
+ content?: string;
220
+ contentHash: string;
221
+ bytes: number;
222
+ }
223
+
224
+ export interface NormalizedAgentAppendixSurface extends NormalizedAppendixSurface {
225
+ agent: GjcSubskillParentAgent;
226
+ }
227
+
228
+ export interface NormalizedGjcPluginSurfaces {
229
+ subskills: NormalizedSubskillSurface[];
230
+ tools: NormalizedToolSurface[];
231
+ hooks: NormalizedHookSurface[];
232
+ mcps: NormalizedMcpSurface[];
233
+ systemAppendices: NormalizedAppendixSurface[];
234
+ agentAppendices: NormalizedAgentAppendixSurface[];
235
+ }
236
+
237
+ /**
238
+ * Result of the pure compile step. Computed from manifest, frontmatter, and
239
+ * declared files read as bytes only — never by importing plugin code.
240
+ */
241
+ export interface NormalizedGjcPluginBundle {
242
+ name: string;
243
+ version: string;
244
+ root: string;
245
+ manifestPath: string;
246
+ manifestHash: string;
247
+ surfaces: NormalizedGjcPluginSurfaces;
248
+ files: GjcPluginCopiedFile[];
249
+ }
250
+
251
+ export interface GjcPluginQuarantineEntry {
252
+ surfaceId: string;
253
+ code: GjcPluginLoadErrorCode;
254
+ message: string;
255
+ detectedAt: string;
256
+ }
257
+
258
+ export interface GjcPluginRegistrySource {
259
+ kind: GjcPluginSourceKind;
260
+ uri: string;
261
+ ref?: string;
262
+ sha?: string;
263
+ resolvedAt: string;
264
+ }
265
+
266
+ export interface GjcPluginRegistryEntry {
267
+ name: string;
268
+ version: string;
269
+ scope: GjcPluginScope;
270
+ enabled: boolean;
271
+ pluginRoot: string;
272
+ manifestPath: string;
273
+ manifestHash: string;
274
+ source: GjcPluginRegistrySource;
275
+ installedAt: string;
276
+ updatedAt: string;
277
+ copiedFiles: GjcPluginCopiedFile[];
278
+ surfaces: NormalizedGjcPluginSurfaces;
279
+ disabledSurfaceIds: string[];
280
+ quarantine?: GjcPluginQuarantineEntry[];
281
+ }
282
+
283
+ export interface GjcPluginRegistry {
284
+ version: 1;
285
+ scope: GjcPluginScope;
286
+ plugins: GjcPluginRegistryEntry[];
287
+ }
288
+
289
+ /**
290
+ * Stable identifiers for plugin-contributed surfaces used by observability,
291
+ * disabledSurfaceIds, and quarantine bookkeeping.
292
+ */
293
+ export type GjcPluginSurfaceExtensionId = string;