@gajae-code/coding-agent 0.3.2 → 0.4.1
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.
- package/CHANGELOG.md +39 -0
- package/dist/types/config/model-registry.d.ts +17 -10
- package/dist/types/config/models-config-schema.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +5 -0
- package/dist/types/edit/diff.d.ts +16 -0
- package/dist/types/edit/modes/replace.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
- package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
- package/dist/types/extensibility/skills.d.ts +9 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
- package/dist/types/harness-control-plane/storage.d.ts +7 -0
- package/dist/types/lsp/client.d.ts +1 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/blob-store.d.ts +17 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/skill-state/active-state.d.ts +13 -0
- package/dist/types/thinking.d.ts +3 -2
- package/dist/types/tools/index.d.ts +3 -0
- package/package.json +9 -7
- package/src/cli.ts +14 -0
- package/src/commands/harness.ts +192 -7
- package/src/commands/ultragoal.ts +1 -21
- package/src/config/model-equivalence.ts +1 -1
- package/src/config/model-registry.ts +32 -5
- package/src/config/models-config-schema.ts +7 -2
- package/src/config/settings-schema.ts +4 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
- package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
- package/src/discovery/claude-plugins.ts +25 -5
- package/src/edit/diff.ts +64 -1
- package/src/edit/modes/replace.ts +60 -2
- package/src/extensibility/gjc-plugins/activation.ts +87 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +114 -0
- package/src/extensibility/gjc-plugins/loader.ts +131 -0
- package/src/extensibility/gjc-plugins/paths.ts +66 -0
- package/src/extensibility/gjc-plugins/schema.ts +79 -0
- package/src/extensibility/gjc-plugins/state.ts +29 -0
- package/src/extensibility/gjc-plugins/tools.ts +47 -0
- package/src/extensibility/gjc-plugins/types.ts +97 -0
- package/src/extensibility/gjc-plugins/validation.ts +76 -0
- package/src/extensibility/skills.ts +39 -7
- package/src/gjc-runtime/state-runtime.ts +93 -2
- package/src/gjc-runtime/state-writer.ts +17 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
- package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
- package/src/gjc-runtime/workflow-manifest.ts +2 -2
- package/src/harness-control-plane/storage.ts +144 -2
- package/src/hashline/hash.ts +23 -0
- package/src/hooks/skill-state.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +7 -0
- package/src/modes/acp/acp-agent.ts +25 -2
- package/src/modes/bridge/bridge-mode.ts +124 -2
- package/src/modes/controllers/input-controller.ts +14 -2
- package/src/modes/prompt-action-autocomplete.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +79 -3
- package/src/modes/rpc/rpc-mode.ts +67 -0
- package/src/modes/rpc/rpc-types.ts +224 -2
- package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
- package/src/modes/shared/agent-wire/command-validation.ts +25 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
- package/src/modes/shared/agent-wire/handshake.ts +43 -3
- package/src/modes/shared/agent-wire/protocol.ts +7 -0
- package/src/modes/shared/agent-wire/responses.ts +2 -2
- package/src/modes/shared/agent-wire/scopes.ts +2 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/system/system-prompt.md +9 -0
- package/src/runtime-mcp/client.ts +7 -4
- package/src/runtime-mcp/manager.ts +45 -13
- package/src/runtime-mcp/transports/http.ts +40 -14
- package/src/runtime-mcp/transports/stdio.ts +11 -10
- package/src/sdk.ts +47 -0
- package/src/session/agent-session.ts +211 -2
- package/src/session/blob-store.ts +84 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-manager.ts +390 -33
- package/src/session/session-storage.ts +26 -0
- package/src/setup/provider-onboarding.ts +2 -2
- package/src/skill-state/active-state.ts +89 -1
- package/src/task/discovery.ts +7 -1
- package/src/task/executor.ts +16 -2
- package/src/thinking.ts +8 -2
- package/src/tools/ask.ts +39 -9
- package/src/tools/index.ts +3 -0
- package/src/tools/skill.ts +15 -3
- package/src/utils/edit-mode.ts +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GJC_PLUGIN_KIND, GjcPluginLoadError, type GjcPluginManifest, type SubskillFrontmatter } from "./types";
|
|
2
|
+
|
|
3
|
+
const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "hooks", "mcp", "mcpServers", "agents"];
|
|
4
|
+
|
|
5
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function requireNonEmptyString(value: unknown, field: string, filePath: string): string {
|
|
10
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
11
|
+
throw new GjcPluginLoadError(
|
|
12
|
+
"invalid_frontmatter",
|
|
13
|
+
`Invalid sub-skill frontmatter in ${filePath}: ${field} must be a non-empty string`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireStringArray(value: unknown, field: string, manifestPath: string): string[] {
|
|
20
|
+
if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
|
|
21
|
+
throw new GjcPluginLoadError(
|
|
22
|
+
"invalid_manifest",
|
|
23
|
+
`Invalid GJC plugin manifest at ${manifestPath}: ${field} must be a string array`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return [...value];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseManifest(raw: unknown, manifestPath: string): GjcPluginManifest {
|
|
30
|
+
if (!isRecord(raw)) {
|
|
31
|
+
throw new GjcPluginLoadError(
|
|
32
|
+
"invalid_manifest",
|
|
33
|
+
`Invalid GJC plugin manifest at ${manifestPath}: expected object`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const key of FORBIDDEN_MANIFEST_KEYS) {
|
|
38
|
+
if (Object.hasOwn(raw, key)) {
|
|
39
|
+
throw new GjcPluginLoadError("forbidden_surface", `Forbidden GJC plugin surface in ${manifestPath}: ${key}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (raw.kind !== GJC_PLUGIN_KIND) {
|
|
44
|
+
throw new GjcPluginLoadError(
|
|
45
|
+
"invalid_kind",
|
|
46
|
+
`Invalid GJC plugin kind in ${manifestPath}: expected ${GJC_PLUGIN_KIND}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: raw.name,
|
|
64
|
+
version: raw.version,
|
|
65
|
+
kind: GJC_PLUGIN_KIND,
|
|
66
|
+
subskills: requireStringArray(raw.subskills, "subskills", manifestPath),
|
|
67
|
+
tools: requireStringArray(raw.tools, "tools", manifestPath),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseSubskillFrontmatter(fm: Record<string, unknown>, filePath: string): SubskillFrontmatter {
|
|
72
|
+
return {
|
|
73
|
+
name: requireNonEmptyString(fm.name, "name", filePath),
|
|
74
|
+
binds_to: requireNonEmptyString(fm.binds_to, "binds_to", filePath),
|
|
75
|
+
phase: requireNonEmptyString(fm.phase, "phase", filePath),
|
|
76
|
+
activation_arg: requireNonEmptyString(fm.activation_arg, "activation_arg", filePath),
|
|
77
|
+
description: requireNonEmptyString(fm.description, "description", filePath),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ActiveSubskillEntry } from "../../skill-state/active-state";
|
|
2
|
+
import { readVisibleSkillActiveState } from "../../skill-state/active-state";
|
|
3
|
+
import type { LoadedSubskillActivation } from "./types";
|
|
4
|
+
|
|
5
|
+
export function toActiveSubskillEntry(activation: LoadedSubskillActivation): ActiveSubskillEntry {
|
|
6
|
+
return {
|
|
7
|
+
plugin: activation.plugin,
|
|
8
|
+
subskillName: activation.subskillName,
|
|
9
|
+
parent: activation.parent,
|
|
10
|
+
bindsTo: activation.bindsTo,
|
|
11
|
+
phase: activation.phase,
|
|
12
|
+
activationArg: activation.activationArg,
|
|
13
|
+
filePath: activation.filePath,
|
|
14
|
+
toolPaths: activation.toolPaths,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readActiveSubskillsForParent(input: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
parent: string;
|
|
22
|
+
phase: string;
|
|
23
|
+
}): Promise<ActiveSubskillEntry[]> {
|
|
24
|
+
const state = await readVisibleSkillActiveState(input.cwd, input.sessionId);
|
|
25
|
+
const parent = input.parent.trim();
|
|
26
|
+
const phase = input.phase.trim();
|
|
27
|
+
if (!state || !parent || !phase) return [];
|
|
28
|
+
return (state.active_subskills ?? []).filter(entry => entry.parent === parent && entry.phase === phase);
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { logger } from "@gajae-code/utils";
|
|
2
|
+
import { loadCustomTools } from "../custom-tools/loader";
|
|
3
|
+
import type { CustomTool } from "../custom-tools/types";
|
|
4
|
+
import { readActiveSubskillsForParent } from "./state";
|
|
5
|
+
|
|
6
|
+
export async function loadActiveSubskillTools(input: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
parent: string;
|
|
10
|
+
phase: string;
|
|
11
|
+
reservedToolNames?: string[];
|
|
12
|
+
}): Promise<CustomTool[]> {
|
|
13
|
+
const entries = await readActiveSubskillsForParent(input);
|
|
14
|
+
const toolPaths = [
|
|
15
|
+
...new Set(entries.flatMap(entry => entry.toolPaths ?? []).filter(path => path.trim().length > 0)),
|
|
16
|
+
];
|
|
17
|
+
if (toolPaths.length === 0) return [];
|
|
18
|
+
|
|
19
|
+
const reservedToolNames = new Set(input.reservedToolNames ?? []);
|
|
20
|
+
const result = await loadCustomTools(
|
|
21
|
+
toolPaths.map(path => ({ path })),
|
|
22
|
+
input.cwd,
|
|
23
|
+
input.reservedToolNames ?? [],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
for (const error of result.errors) {
|
|
27
|
+
logger.warn("Skipping GJC plugin sub-skill tool", { path: error.path, error: error.error });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tools: CustomTool[] = [];
|
|
31
|
+
const seenNames = new Set<string>();
|
|
32
|
+
for (const loadedTool of result.tools) {
|
|
33
|
+
const name = loadedTool.tool.name;
|
|
34
|
+
if (reservedToolNames.has(name)) {
|
|
35
|
+
logger.warn("Skipping GJC plugin sub-skill tool name because it conflicts with a reserved tool", { name });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (seenNames.has(name)) {
|
|
39
|
+
logger.warn("Skipping duplicate GJC plugin sub-skill tool name", { name, path: loadedTool.path });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seenNames.add(name);
|
|
43
|
+
tools.push(loadedTool.tool);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return tools;
|
|
47
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CanonicalGjcWorkflowSkill } from "../../skill-state/active-state";
|
|
2
|
+
import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../../skill-state/active-state";
|
|
3
|
+
|
|
4
|
+
export const GJC_PLUGIN_MANIFEST_FILENAME = "gajae-plugin.json";
|
|
5
|
+
export const GJC_PLUGIN_KIND = "gajae-code-plugin";
|
|
6
|
+
|
|
7
|
+
export const GJC_SUBSKILL_PARENT_SKILLS = CANONICAL_GJC_WORKFLOW_SKILLS;
|
|
8
|
+
export type GjcSubskillParentSkill = CanonicalGjcWorkflowSkill;
|
|
9
|
+
|
|
10
|
+
export const GJC_SUBSKILL_PARENT_AGENTS = ["executor", "architect", "planner", "critic"] as const;
|
|
11
|
+
export type GjcSubskillParentAgent = (typeof GJC_SUBSKILL_PARENT_AGENTS)[number];
|
|
12
|
+
|
|
13
|
+
export type GjcSubskillParent = GjcSubskillParentSkill | GjcSubskillParentAgent;
|
|
14
|
+
|
|
15
|
+
export const GJC_AGENT_SUBSKILL_PHASES: Record<GjcSubskillParentAgent, string[]> = {
|
|
16
|
+
executor: ["prompt"],
|
|
17
|
+
architect: ["prompt"],
|
|
18
|
+
planner: ["prompt"],
|
|
19
|
+
critic: ["prompt"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface GjcPluginManifest {
|
|
23
|
+
name: string;
|
|
24
|
+
version: string;
|
|
25
|
+
kind: "gajae-code-plugin";
|
|
26
|
+
subskills: string[];
|
|
27
|
+
tools: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SubskillFrontmatter {
|
|
31
|
+
name: string;
|
|
32
|
+
binds_to: string;
|
|
33
|
+
phase: string;
|
|
34
|
+
activation_arg: string;
|
|
35
|
+
description: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LoadedSubskillBinding {
|
|
39
|
+
plugin: string;
|
|
40
|
+
subskillName: string;
|
|
41
|
+
parent: string;
|
|
42
|
+
bindsTo: string;
|
|
43
|
+
phase: string;
|
|
44
|
+
activationArg: string;
|
|
45
|
+
description: string;
|
|
46
|
+
filePath: string;
|
|
47
|
+
body: string;
|
|
48
|
+
toolPaths: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LoadedSubskillActivation {
|
|
52
|
+
activationArg: string;
|
|
53
|
+
plugin: string;
|
|
54
|
+
subskillName: string;
|
|
55
|
+
parent: string;
|
|
56
|
+
bindsTo: string;
|
|
57
|
+
phase: string;
|
|
58
|
+
filePath: string;
|
|
59
|
+
toolPaths: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PhaseScopedToolBinding {
|
|
63
|
+
plugin: string;
|
|
64
|
+
parent: string;
|
|
65
|
+
phase: string;
|
|
66
|
+
toolPath: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LoadedGjcPlugin {
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
root: string;
|
|
73
|
+
manifestPath: string;
|
|
74
|
+
bindings: LoadedSubskillBinding[];
|
|
75
|
+
toolBindings: PhaseScopedToolBinding[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type GjcPluginLoadErrorCode =
|
|
79
|
+
| "forbidden_surface"
|
|
80
|
+
| "invalid_manifest"
|
|
81
|
+
| "invalid_frontmatter"
|
|
82
|
+
| "invalid_parent"
|
|
83
|
+
| "invalid_phase"
|
|
84
|
+
| "duplicate_arg"
|
|
85
|
+
| "duplicate_parent_phase"
|
|
86
|
+
| "missing_file"
|
|
87
|
+
| "invalid_kind";
|
|
88
|
+
|
|
89
|
+
export class GjcPluginLoadError extends Error {
|
|
90
|
+
readonly code: GjcPluginLoadErrorCode;
|
|
91
|
+
|
|
92
|
+
constructor(code: GjcPluginLoadErrorCode, message: string, options?: ErrorOptions) {
|
|
93
|
+
super(message, options);
|
|
94
|
+
this.name = "GjcPluginLoadError";
|
|
95
|
+
this.code = code;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isKnownWorkflowState } from "../../gjc-runtime/workflow-manifest";
|
|
2
|
+
import type { CanonicalGjcWorkflowSkill } from "../../skill-state/active-state";
|
|
3
|
+
import {
|
|
4
|
+
GJC_AGENT_SUBSKILL_PHASES,
|
|
5
|
+
GJC_SUBSKILL_PARENT_AGENTS,
|
|
6
|
+
GJC_SUBSKILL_PARENT_SKILLS,
|
|
7
|
+
GjcPluginLoadError,
|
|
8
|
+
type GjcSubskillParentAgent,
|
|
9
|
+
type LoadedSubskillBinding,
|
|
10
|
+
type SubskillFrontmatter,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
function isParentSkill(value: string): value is CanonicalGjcWorkflowSkill {
|
|
14
|
+
return (GJC_SUBSKILL_PARENT_SKILLS as readonly string[]).includes(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isParentAgent(value: string): value is GjcSubskillParentAgent {
|
|
18
|
+
return (GJC_SUBSKILL_PARENT_AGENTS as readonly string[]).includes(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateBinding(fm: SubskillFrontmatter): void {
|
|
22
|
+
const parent = fm.binds_to;
|
|
23
|
+
if (isParentSkill(parent)) {
|
|
24
|
+
if (!isKnownWorkflowState(parent, fm.phase)) {
|
|
25
|
+
throw new GjcPluginLoadError("invalid_phase", `Invalid GJC sub-skill phase for ${parent}: ${fm.phase}`);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isParentAgent(parent)) {
|
|
31
|
+
if (!GJC_AGENT_SUBSKILL_PHASES[parent].includes(fm.phase)) {
|
|
32
|
+
throw new GjcPluginLoadError("invalid_phase", `Invalid GJC sub-skill phase for ${parent}: ${fm.phase}`);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new GjcPluginLoadError("invalid_parent", `Invalid GJC sub-skill parent: ${parent}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildParentArgMap(
|
|
41
|
+
bindings: readonly LoadedSubskillBinding[],
|
|
42
|
+
): Map<string, Map<string, LoadedSubskillBinding>> {
|
|
43
|
+
const byParent = new Map<string, Map<string, LoadedSubskillBinding>>();
|
|
44
|
+
for (const binding of bindings) {
|
|
45
|
+
let byArg = byParent.get(binding.parent);
|
|
46
|
+
if (!byArg) {
|
|
47
|
+
byArg = new Map<string, LoadedSubskillBinding>();
|
|
48
|
+
byParent.set(binding.parent, byArg);
|
|
49
|
+
}
|
|
50
|
+
const existing = byArg.get(binding.activationArg);
|
|
51
|
+
if (existing) {
|
|
52
|
+
throw new GjcPluginLoadError(
|
|
53
|
+
"duplicate_arg",
|
|
54
|
+
`Duplicate GJC sub-skill activation_arg for ${binding.parent}: ${binding.activationArg} (${existing.filePath}, ${binding.filePath})`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
byArg.set(binding.activationArg, binding);
|
|
58
|
+
}
|
|
59
|
+
return byParent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildParentPhaseSet(bindings: readonly LoadedSubskillBinding[]): Set<string> {
|
|
63
|
+
const seen = new Map<string, LoadedSubskillBinding>();
|
|
64
|
+
for (const binding of bindings) {
|
|
65
|
+
const key = `${binding.parent}\u0000${binding.phase}`;
|
|
66
|
+
const existing = seen.get(key);
|
|
67
|
+
if (existing) {
|
|
68
|
+
throw new GjcPluginLoadError(
|
|
69
|
+
"duplicate_parent_phase",
|
|
70
|
+
`Duplicate GJC sub-skill parent/phase binding for ${binding.parent}/${binding.phase} (${existing.filePath}, ${binding.filePath})`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
seen.set(key, binding);
|
|
74
|
+
}
|
|
75
|
+
return new Set(seen.keys());
|
|
76
|
+
}
|
|
@@ -8,6 +8,8 @@ import { type Skill as CapabilitySkill, loadCapability } from "../discovery";
|
|
|
8
8
|
import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
|
|
9
9
|
import type { SkillPromptDetails } from "../session/messages";
|
|
10
10
|
import { expandTilde } from "../tools/path-utils";
|
|
11
|
+
import type { LoadedSubskillActivation } from "./gjc-plugins";
|
|
12
|
+
import { buildSubskillInjection } from "./gjc-plugins/injection";
|
|
11
13
|
export interface Skill {
|
|
12
14
|
name: string;
|
|
13
15
|
description: string;
|
|
@@ -280,6 +282,14 @@ export interface BuiltSkillPromptMessage {
|
|
|
280
282
|
details: SkillPromptDetails;
|
|
281
283
|
}
|
|
282
284
|
|
|
285
|
+
export interface BuildSkillPromptMessageContext {
|
|
286
|
+
subskillActivation?: LoadedSubskillActivation;
|
|
287
|
+
subskillActivationSet?: LoadedSubskillActivation[];
|
|
288
|
+
currentPhase?: string;
|
|
289
|
+
cwd?: string;
|
|
290
|
+
sessionId?: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
283
293
|
export function getSkillSlashCommandName(skill: Pick<Skill, "name">): string {
|
|
284
294
|
return `skill:${skill.name}`;
|
|
285
295
|
}
|
|
@@ -370,6 +380,7 @@ export function resolveSkillSlashCommands(
|
|
|
370
380
|
export async function buildSkillPromptMessage(
|
|
371
381
|
skill: Pick<Skill, "name" | "filePath" | "content">,
|
|
372
382
|
args: string,
|
|
383
|
+
context?: BuildSkillPromptMessageContext,
|
|
373
384
|
): Promise<BuiltSkillPromptMessage> {
|
|
374
385
|
const content = typeof skill.content === "string" ? skill.content : await Bun.file(skill.filePath).text();
|
|
375
386
|
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
@@ -378,14 +389,35 @@ export async function buildSkillPromptMessage(
|
|
|
378
389
|
if (trimmedArgs) {
|
|
379
390
|
metaLines.push(`User: ${trimmedArgs}`);
|
|
380
391
|
}
|
|
381
|
-
|
|
392
|
+
let message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
393
|
+
const details: SkillPromptDetails = {
|
|
394
|
+
name: skill.name,
|
|
395
|
+
path: skill.filePath,
|
|
396
|
+
args: trimmedArgs || undefined,
|
|
397
|
+
lineCount: body ? body.split("\n").length : 0,
|
|
398
|
+
};
|
|
399
|
+
if (context?.subskillActivationSet) {
|
|
400
|
+
details.subskillActivationSet = context.subskillActivationSet;
|
|
401
|
+
}
|
|
402
|
+
if (context) {
|
|
403
|
+
const injection = context.cwd
|
|
404
|
+
? await buildSubskillInjection({
|
|
405
|
+
cwd: context.cwd,
|
|
406
|
+
sessionId: context.sessionId,
|
|
407
|
+
skillName: skill.name,
|
|
408
|
+
activation: context.subskillActivation,
|
|
409
|
+
currentPhase: context.currentPhase,
|
|
410
|
+
})
|
|
411
|
+
: null;
|
|
412
|
+
if (injection) {
|
|
413
|
+
message += injection.block;
|
|
414
|
+
details.subskillActivation = injection.details ?? context.subskillActivation;
|
|
415
|
+
} else if (context.subskillActivation) {
|
|
416
|
+
details.subskillActivation = context.subskillActivation;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
382
419
|
return {
|
|
383
420
|
message,
|
|
384
|
-
details
|
|
385
|
-
name: skill.name,
|
|
386
|
-
path: skill.filePath,
|
|
387
|
-
args: trimmedArgs || undefined,
|
|
388
|
-
lineCount: body ? body.split("\n").length : 0,
|
|
389
|
-
},
|
|
421
|
+
details,
|
|
390
422
|
};
|
|
391
423
|
}
|