@gajae-code/coding-agent 0.6.4 → 0.6.5

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 (120) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/types/cli/migrate-cli.d.ts +20 -0
  3. package/dist/types/commands/migrate.d.ts +33 -0
  4. package/dist/types/config/keybindings.d.ts +4 -0
  5. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  6. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  7. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  8. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  9. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  12. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  13. package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
  14. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
  15. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  16. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  17. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  18. package/dist/types/hooks/skill-state.d.ts +12 -4
  19. package/dist/types/migrate/action-planner.d.ts +11 -0
  20. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  21. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  22. package/dist/types/migrate/adapters/index.d.ts +45 -0
  23. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  24. package/dist/types/migrate/executor.d.ts +2 -0
  25. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  26. package/dist/types/migrate/report.d.ts +18 -0
  27. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  28. package/dist/types/migrate/types.d.ts +126 -0
  29. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  30. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  31. package/dist/types/research-plan/index.d.ts +1 -0
  32. package/dist/types/research-plan/ledger.d.ts +33 -0
  33. package/dist/types/rlm/artifacts.d.ts +1 -1
  34. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  35. package/dist/types/skill-state/active-state.d.ts +6 -11
  36. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  37. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  38. package/dist/types/task/spawn-gate.d.ts +1 -10
  39. package/package.json +7 -7
  40. package/src/cli/migrate-cli.ts +106 -0
  41. package/src/cli.ts +1 -0
  42. package/src/commands/deep-interview.ts +2 -2
  43. package/src/commands/migrate.ts +46 -0
  44. package/src/commands/state.ts +2 -1
  45. package/src/commands/team.ts +7 -3
  46. package/src/coordinator-mcp/policy.ts +10 -2
  47. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  48. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  49. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  50. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  51. package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
  52. package/src/extensibility/custom-commands/loader.ts +0 -7
  53. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  54. package/src/extensibility/gjc-plugins/state.ts +16 -1
  55. package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
  56. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  57. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  58. package/src/gjc-runtime/launch-tmux.ts +6 -1
  59. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  60. package/src/gjc-runtime/session-layout.ts +180 -0
  61. package/src/gjc-runtime/session-resolution.ts +217 -0
  62. package/src/gjc-runtime/state-graph.ts +1 -2
  63. package/src/gjc-runtime/state-migrations.ts +1 -0
  64. package/src/gjc-runtime/state-runtime.ts +230 -121
  65. package/src/gjc-runtime/state-schema.ts +2 -0
  66. package/src/gjc-runtime/state-writer.ts +289 -41
  67. package/src/gjc-runtime/team-runtime.ts +43 -19
  68. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  69. package/src/gjc-runtime/ultragoal-guard.ts +45 -2
  70. package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
  71. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  72. package/src/gjc-runtime/workflow-manifest.ts +1 -2
  73. package/src/harness-control-plane/storage.ts +14 -4
  74. package/src/hooks/native-skill-hook.ts +38 -12
  75. package/src/hooks/skill-state.ts +178 -83
  76. package/src/internal-urls/docs-index.generated.ts +6 -4
  77. package/src/migrate/action-planner.ts +318 -0
  78. package/src/migrate/adapters/claude-code.ts +39 -0
  79. package/src/migrate/adapters/codex.ts +70 -0
  80. package/src/migrate/adapters/index.ts +277 -0
  81. package/src/migrate/adapters/opencode.ts +52 -0
  82. package/src/migrate/executor.ts +81 -0
  83. package/src/migrate/mcp-mapper.ts +152 -0
  84. package/src/migrate/report.ts +104 -0
  85. package/src/migrate/skill-normalizer.ts +80 -0
  86. package/src/migrate/types.ts +163 -0
  87. package/src/modes/bridge/bridge-mode.ts +2 -2
  88. package/src/modes/components/custom-editor.ts +30 -20
  89. package/src/modes/rpc/rpc-mode.ts +2 -2
  90. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  91. package/src/prompts/agents/init.md +1 -1
  92. package/src/prompts/system/plan-mode-active.md +1 -1
  93. package/src/prompts/tools/ast-grep.md +1 -1
  94. package/src/prompts/tools/search.md +1 -1
  95. package/src/prompts/tools/task.md +1 -2
  96. package/src/research-plan/index.ts +1 -0
  97. package/src/research-plan/ledger.ts +177 -0
  98. package/src/rlm/artifacts.ts +12 -3
  99. package/src/rlm/index.ts +7 -0
  100. package/src/runtime-mcp/config-writer.ts +46 -0
  101. package/src/session/agent-session.ts +15 -21
  102. package/src/setup/hermes-setup.ts +1 -1
  103. package/src/skill-state/active-state.ts +72 -108
  104. package/src/skill-state/canonical-skills.ts +4 -0
  105. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  106. package/src/skill-state/workflow-hud.ts +4 -2
  107. package/src/skill-state/workflow-state-contract.ts +3 -3
  108. package/src/task/agents.ts +1 -22
  109. package/src/task/index.ts +1 -41
  110. package/src/task/spawn-gate.ts +1 -38
  111. package/src/task/types.ts +1 -1
  112. package/src/tools/ask.ts +34 -12
  113. package/src/tools/computer.ts +58 -4
  114. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  115. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  116. package/src/prompts/agents/explore.md +0 -58
  117. package/src/prompts/agents/plan.md +0 -49
  118. package/src/prompts/agents/reviewer.md +0 -141
  119. package/src/prompts/agents/task.md +0 -16
  120. package/src/prompts/review-request.md +0 -70
@@ -0,0 +1,52 @@
1
+ /**
2
+ * OpenCode adapter: reads `~/.config/opencode/opencode.json` (mcp), plus
3
+ * `~/.config/opencode/skills` and `~/.config/opencode/commands`.
4
+ */
5
+ import * as path from "node:path";
6
+ import type { AdapterResult, McpCandidate } from "../types";
7
+ import {
8
+ type Adapter,
9
+ type AdapterOptions,
10
+ collectMarkdownPrompts,
11
+ collectSkillTree,
12
+ parseSourceJson,
13
+ readSourceText,
14
+ } from "./index";
15
+
16
+ const SOURCE = "opencode" as const;
17
+
18
+ export const opencodeAdapter: Adapter = {
19
+ source: SOURCE,
20
+ async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
21
+ const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
22
+ const baseDir = path.join(homeDir, ".config", "opencode");
23
+
24
+ const configPath = path.join(baseDir, "opencode.json");
25
+ const read = await readSourceText(configPath, SOURCE, "mcp");
26
+ if ("diagnostic" in read) {
27
+ result.diagnostics.push(read.diagnostic);
28
+ } else {
29
+ const parsed = parseSourceJson(read.text, configPath, SOURCE, "mcp");
30
+ if ("diagnostic" in parsed) {
31
+ result.diagnostics.push(parsed.diagnostic);
32
+ } else {
33
+ const servers = parsed.data.mcp;
34
+ if (servers && typeof servers === "object" && !Array.isArray(servers)) {
35
+ for (const [name, raw] of Object.entries(servers as Record<string, unknown>)) {
36
+ result.mcpCandidates.push({ source: SOURCE, name, raw } satisfies McpCandidate);
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ const skills = await collectSkillTree(path.join(baseDir, "skills"), SOURCE);
43
+ result.skillCandidates.push(...skills.candidates);
44
+ result.diagnostics.push(...skills.diagnostics);
45
+
46
+ const commands = await collectMarkdownPrompts(path.join(baseDir, "commands"), SOURCE);
47
+ result.skillCandidates.push(...commands.candidates);
48
+ result.diagnostics.push(...commands.diagnostics);
49
+
50
+ return result;
51
+ },
52
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Execute planned migration actions.
3
+ *
4
+ * Consumes the planner's actions unchanged and performs only `create`/`update`
5
+ * operations. It never re-plans. Writes are not transactional: on a write error
6
+ * the offending action flips to `failed_io`, already-written actions remain, and
7
+ * remaining actions still run (no rollback). Dry-run never calls this.
8
+ */
9
+ import * as fsSync from "node:fs";
10
+ import * as fs from "node:fs/promises";
11
+ import * as path from "node:path";
12
+ import { upsertMCPServer } from "../runtime-mcp/config-writer";
13
+ import type { MigrateAction } from "./types";
14
+
15
+ async function ensureRealDirectoryPathNoFollow(directory: string): Promise<void> {
16
+ const resolved = path.resolve(directory);
17
+ const parsed = path.parse(resolved);
18
+ let current = parsed.root;
19
+ for (const part of resolved.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
20
+ current = path.join(current, part);
21
+ try {
22
+ const stat = await fs.lstat(current);
23
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
24
+ throw new Error(`skill destination ancestor is not a real directory: ${current}`);
25
+ }
26
+ } catch (error) {
27
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
28
+ await fs.mkdir(current);
29
+ const stat = await fs.lstat(current);
30
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
31
+ throw new Error(`skill destination ancestor is not a real directory: ${current}`);
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ async function writeSkillFileNoFollow(destination: string, content: string): Promise<void> {
38
+ const skillDir = path.dirname(destination);
39
+ await ensureRealDirectoryPathNoFollow(skillDir);
40
+ const handle = await fs.open(
41
+ destination,
42
+ fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW,
43
+ 0o666,
44
+ );
45
+ try {
46
+ await handle.writeFile(content, "utf-8");
47
+ } finally {
48
+ await handle.close();
49
+ }
50
+ }
51
+
52
+ export async function executeActions(actions: MigrateAction[]): Promise<MigrateAction[]> {
53
+ const out: MigrateAction[] = [];
54
+ for (const action of actions) {
55
+ if (action.operation !== "create" && action.operation !== "update") {
56
+ out.push(action);
57
+ continue;
58
+ }
59
+ try {
60
+ if (action.type === "mcp" && action.mcp && action.name && action.destination) {
61
+ await upsertMCPServer(action.destination, action.name, action.mcp.config, {
62
+ force: action.operation === "update",
63
+ });
64
+ out.push(action);
65
+ } else if (action.type === "skill" && action.skill && action.destination) {
66
+ await writeSkillFileNoFollow(action.destination, action.skill.content);
67
+ out.push(action);
68
+ } else {
69
+ out.push(action);
70
+ }
71
+ } catch (error) {
72
+ out.push({
73
+ ...action,
74
+ operation: "fail",
75
+ status: "failed_io",
76
+ reason: `write failed: ${(error as Error).message}`,
77
+ });
78
+ }
79
+ }
80
+ return out;
81
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Map raw source MCP server entries onto GJC `MCPServerConfig`.
3
+ *
4
+ * Implements the source-schema compatibility matrix from the consensus plan:
5
+ * preserved (P), transformed (T), omitted-with-warning (OW), skipped (S,
6
+ * `skipped_unmappable`), and failed (F, `failed_invalid_source`). Secret-indirection
7
+ * fields are always omitted-with-warning; their values are never read or emitted.
8
+ */
9
+ import type { MCPServerConfig } from "../runtime-mcp/types";
10
+ import type { MigrateSource } from "./types";
11
+
12
+ export type McpMapOutcome =
13
+ | { ok: true; config: MCPServerConfig; warnings: string[] }
14
+ | { ok: false; status: "skipped_unmappable" | "failed_invalid_source"; reason: string };
15
+
16
+ function isRecord(value: unknown): value is Record<string, unknown> {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+
20
+ function asStringArray(value: unknown): string[] | undefined | "invalid" {
21
+ if (value === undefined) return undefined;
22
+ if (!Array.isArray(value) || !value.every(v => typeof v === "string")) return "invalid";
23
+ return value as string[];
24
+ }
25
+
26
+ function asStringRecord(value: unknown): Record<string, string> | undefined | "invalid" {
27
+ if (value === undefined) return undefined;
28
+ if (!isRecord(value) || !Object.values(value).every(v => typeof v === "string")) return "invalid";
29
+ return value as Record<string, string>;
30
+ }
31
+
32
+ /** Secret-indirection fields per source: always omitted-with-warning, never read. */
33
+ const SECRET_INDIRECTION_FIELDS: Record<MigrateSource, string[]> = {
34
+ "claude-code": [],
35
+ codex: ["env_vars", "env_http_headers", "bearer_token_env_var"],
36
+ opencode: [],
37
+ };
38
+
39
+ /** Fields recognized for a source (handled or intentionally omitted). Anything else is omitted-with-warning. */
40
+ const RECOGNIZED_FIELDS: Record<MigrateSource, ReadonlySet<string>> = {
41
+ "claude-code": new Set(["type", "command", "args", "env", "url", "headers", "enabled", "timeout", "cwd"]),
42
+ codex: new Set([
43
+ "type",
44
+ "command",
45
+ "args",
46
+ "env",
47
+ "url",
48
+ "http_headers",
49
+ "cwd",
50
+ "enabled",
51
+ "timeout",
52
+ "tool_timeout_sec",
53
+ // omitted-with-warning fields are still "recognized" (handled below):
54
+ "env_vars",
55
+ "env_http_headers",
56
+ "bearer_token_env_var",
57
+ "startup_timeout_sec",
58
+ "enabled_tools",
59
+ "disabled_tools",
60
+ ]),
61
+ opencode: new Set(["type", "command", "args", "env", "url", "headers", "enabled", "timeout", "cwd"]),
62
+ };
63
+
64
+ /** Fields with no GJC equivalent: omitted-with-warning (named explicitly so the warning is precise). */
65
+ const OMITTED_FIELDS: Record<MigrateSource, string[]> = {
66
+ "claude-code": [],
67
+ codex: ["startup_timeout_sec", "enabled_tools", "disabled_tools"],
68
+ opencode: [],
69
+ };
70
+
71
+ export function mapMcpEntry(source: MigrateSource, name: string, raw: unknown): McpMapOutcome {
72
+ if (!isRecord(raw)) {
73
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" is not an object` };
74
+ }
75
+
76
+ const warnings: string[] = [];
77
+ for (const field of SECRET_INDIRECTION_FIELDS[source]) {
78
+ if (field in raw) warnings.push(`omitted secret-indirection field "${field}" for "${name}" (value not read)`);
79
+ }
80
+ for (const field of OMITTED_FIELDS[source]) {
81
+ if (field in raw) warnings.push(`omitted unsupported field "${field}" for "${name}"`);
82
+ }
83
+ // Unknown/unrecognized fields: omit-with-warning (never copy their values).
84
+ for (const field of Object.keys(raw)) {
85
+ if (!RECOGNIZED_FIELDS[source].has(field)) {
86
+ warnings.push(`omitted unknown field "${field}" for "${name}"`);
87
+ }
88
+ }
89
+
90
+ const rawType = typeof raw.type === "string" ? raw.type : undefined;
91
+ const command = typeof raw.command === "string" ? raw.command : undefined;
92
+ const url = typeof raw.url === "string" ? raw.url : undefined;
93
+
94
+ const base: { enabled?: boolean; timeout?: number } = {};
95
+ if (typeof raw.enabled === "boolean") base.enabled = raw.enabled;
96
+ if (typeof raw.timeout === "number") base.timeout = raw.timeout;
97
+ // Codex tool_timeout_sec -> timeout (ms).
98
+ if (source === "codex" && typeof raw.tool_timeout_sec === "number") {
99
+ base.timeout = raw.tool_timeout_sec * 1000;
100
+ }
101
+
102
+ const wantsStdio =
103
+ source === "opencode" ? rawType === "local" || (!rawType && !!command) : !!command || rawType === "stdio";
104
+ const wantsHttp =
105
+ source === "opencode"
106
+ ? rawType === "remote" || rawType === "sse"
107
+ : rawType === "http" || rawType === "sse" || (!command && !!url);
108
+
109
+ if (wantsStdio) {
110
+ if (!command) {
111
+ return {
112
+ ok: false,
113
+ status: "skipped_unmappable",
114
+ reason: `server "${name}" has no command for stdio transport`,
115
+ };
116
+ }
117
+ const args = asStringArray(raw.args);
118
+ if (args === "invalid") {
119
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" has invalid "args"` };
120
+ }
121
+ const env = asStringRecord(raw.env);
122
+ if (env === "invalid") {
123
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" has invalid "env"` };
124
+ }
125
+ const config: MCPServerConfig = { type: "stdio", command, ...base };
126
+ if (args) config.args = args;
127
+ if (env) config.env = env;
128
+ if (typeof raw.cwd === "string") config.cwd = raw.cwd;
129
+ return { ok: true, config, warnings };
130
+ }
131
+
132
+ if (wantsHttp) {
133
+ if (!url) {
134
+ return {
135
+ ok: false,
136
+ status: "skipped_unmappable",
137
+ reason: `server "${name}" has no url for http/sse transport`,
138
+ };
139
+ }
140
+ const headerSource = source === "codex" ? raw.http_headers : raw.headers;
141
+ const headers = asStringRecord(headerSource);
142
+ if (headers === "invalid") {
143
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" has invalid headers` };
144
+ }
145
+ const type = rawType === "sse" ? "sse" : "http";
146
+ const config = { type, url, ...base } as MCPServerConfig;
147
+ if (headers) (config as { headers?: Record<string, string> }).headers = headers;
148
+ return { ok: true, config, warnings };
149
+ }
150
+
151
+ return { ok: false, status: "skipped_unmappable", reason: `server "${name}" has neither a usable command nor url` };
152
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Build the human-readable and `--json` reports for `gjc migrate`.
3
+ *
4
+ * Secret values are never read upstream, so the report only ever contains field
5
+ * names in warnings — but rendering still treats action/warning text as opaque.
6
+ */
7
+ import {
8
+ emptyStatusCounts,
9
+ isFailureStatus,
10
+ type MigrateAction,
11
+ type MigrateDestinations,
12
+ type MigrateReport,
13
+ type MigrateSource,
14
+ type MigrateWarning,
15
+ type StatusCounts,
16
+ } from "./types";
17
+
18
+ export interface BuildReportInput {
19
+ actions: MigrateAction[];
20
+ warnings: MigrateWarning[];
21
+ sources: MigrateSource[];
22
+ destinations: MigrateDestinations;
23
+ dryRun: boolean;
24
+ project: boolean;
25
+ force: boolean;
26
+ }
27
+
28
+ export function buildReport(input: BuildReportInput): MigrateReport {
29
+ const total = emptyStatusCounts();
30
+ const byType = { mcp: emptyStatusCounts(), skill: emptyStatusCounts(), source: emptyStatusCounts() };
31
+ const bySource = {} as Record<MigrateSource, StatusCounts>;
32
+ for (const source of input.sources) bySource[source] = emptyStatusCounts();
33
+
34
+ let ok = true;
35
+ for (const action of input.actions) {
36
+ total[action.status] += 1;
37
+ byType[action.type][action.status] += 1;
38
+ if (bySource[action.source]) bySource[action.source][action.status] += 1;
39
+ if (isFailureStatus(action.status)) ok = false;
40
+ }
41
+
42
+ return {
43
+ ok,
44
+ dryRun: input.dryRun,
45
+ project: input.project,
46
+ force: input.force,
47
+ sources: input.sources,
48
+ destinations: input.destinations,
49
+ summary: { total, byType, bySource },
50
+ actions: input.actions.map(action => ({
51
+ source: action.source,
52
+ type: action.type,
53
+ name: action.name,
54
+ effectiveName: action.effectiveName,
55
+ destination: action.destination,
56
+ operation: action.operation,
57
+ status: action.status,
58
+ reason: action.reason,
59
+ warnings: action.warnings,
60
+ })),
61
+ warnings: input.warnings,
62
+ };
63
+ }
64
+
65
+ function summarizeCounts(counts: StatusCounts): string {
66
+ const parts: string[] = [];
67
+ for (const [status, count] of Object.entries(counts)) {
68
+ if (count > 0) parts.push(`${status}=${count}`);
69
+ }
70
+ return parts.length > 0 ? parts.join(", ") : "nothing";
71
+ }
72
+
73
+ export function renderHuman(report: MigrateReport): string {
74
+ const lines: string[] = [];
75
+ const mode = report.dryRun ? " (dry-run)" : "";
76
+ lines.push(`gjc migrate${mode}: ${report.ok ? "ok" : "completed with failures"}`);
77
+ lines.push(`Sources: ${report.sources.join(", ") || "none"}`);
78
+ lines.push(`Destination: mcp=${report.destinations.mcpConfigPath} skills=${report.destinations.skillsDir}`);
79
+ lines.push("");
80
+ lines.push(`MCP: ${summarizeCounts(report.summary.byType.mcp)}`);
81
+ lines.push(`Skills: ${summarizeCounts(report.summary.byType.skill)}`);
82
+ if (Object.values(report.summary.byType.source).some(n => n > 0)) {
83
+ lines.push(`Source: ${summarizeCounts(report.summary.byType.source)}`);
84
+ }
85
+
86
+ const failures = report.actions.filter(a => isFailureStatus(a.status));
87
+ if (failures.length > 0) {
88
+ lines.push("");
89
+ lines.push("Failures:");
90
+ for (const f of failures) {
91
+ lines.push(` - [${f.source}] ${f.type} ${f.name ?? ""}: ${f.status}${f.reason ? ` (${f.reason})` : ""}`);
92
+ }
93
+ }
94
+
95
+ if (report.warnings.length > 0) {
96
+ lines.push("");
97
+ lines.push("Warnings:");
98
+ for (const w of report.warnings) {
99
+ lines.push(` - [${w.source}] ${w.type} ${w.name ?? ""}: ${w.message}`);
100
+ }
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Normalize a skill from another agent into a native GJC `SKILL.md`.
3
+ *
4
+ * GJC derives a skill's loaded name from its directory (`<slug>/SKILL.md`) when no
5
+ * frontmatter `name` is present, and requires a `description`. To guarantee the
6
+ * effective loaded name equals the lowercase-hyphen slug, we drop any frontmatter
7
+ * `name` and place the file at `<slug>/SKILL.md`, synthesizing a `description`
8
+ * when the source lacks one.
9
+ */
10
+
11
+ import { parseFrontmatter } from "@gajae-code/utils";
12
+ import { YAML } from "bun";
13
+
14
+ export interface NormalizeSkillInput {
15
+ /** Raw name from the source (filename stem, frontmatter name, etc.). */
16
+ rawName: string;
17
+ /** Full source markdown (may or may not have frontmatter). */
18
+ content: string;
19
+ }
20
+
21
+ export interface NormalizedSkill {
22
+ slug: string;
23
+ content: string;
24
+ warnings: string[];
25
+ }
26
+
27
+ /** Convert an arbitrary name into a lowercase-hyphen slug. */
28
+ export function slugify(name: string): string {
29
+ const slug = name
30
+ .normalize("NFKD")
31
+ .replace(/[^\w\s-]/g, "")
32
+ .trim()
33
+ .toLowerCase()
34
+ .replace(/[\s_]+/g, "-")
35
+ .replace(/-+/g, "-")
36
+ .replace(/^-+|-+$/g, "");
37
+ return slug;
38
+ }
39
+
40
+ function firstNonEmptyLine(body: string): string | undefined {
41
+ for (const raw of body.split("\n")) {
42
+ const line = raw.replace(/^#+\s*/, "").trim();
43
+ if (line) return line;
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ /**
49
+ * Produce a `{ slug, content }` pair whose effective GJC-loaded name equals `slug`.
50
+ * Throws only on an unusable name (cannot produce a slug).
51
+ */
52
+ export function normalizeSkill(input: NormalizeSkillInput): NormalizedSkill {
53
+ const warnings: string[] = [];
54
+ const { frontmatter, body } = parseFrontmatter(input.content, { level: "off" });
55
+
56
+ const sourceName =
57
+ typeof frontmatter.name === "string" && frontmatter.name.trim() ? frontmatter.name : input.rawName;
58
+ const slug = slugify(sourceName);
59
+ if (!slug) {
60
+ throw new Error(`cannot derive a valid slug from skill name "${input.rawName}"`);
61
+ }
62
+ if (slugify(input.rawName) !== slug && typeof frontmatter.name === "string") {
63
+ warnings.push(`renamed skill "${input.rawName}" to slug "${slug}"`);
64
+ }
65
+
66
+ // Build the destination frontmatter: drop `name` (loaded name comes from the dir),
67
+ // keep other fields, and ensure a non-empty description.
68
+ const { name: _droppedName, description: rawDescription, ...rest } = frontmatter;
69
+ let description = typeof rawDescription === "string" ? rawDescription.trim() : "";
70
+ if (!description) {
71
+ description = firstNonEmptyLine(body) ?? `Imported ${slug} skill.`;
72
+ warnings.push(`synthesized description for skill "${slug}"`);
73
+ }
74
+
75
+ const fm: Record<string, unknown> = { description, ...rest };
76
+ const yaml = YAML.stringify(fm).trimEnd();
77
+ const content = `---\n${yaml}\n---\n\n${body.trim()}\n`;
78
+
79
+ return { slug, content, warnings };
80
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Shared types for `gjc migrate`.
3
+ *
4
+ * Imports MCP servers and skills from other coding agents (Claude Code, Codex,
5
+ * OpenCode) into native GJC config. See the consensus plan under
6
+ * `.gjc/plans/ralplan/` for the full taxonomy and force/collision semantics.
7
+ */
8
+ import type { MCPServerConfig } from "../runtime-mcp/types";
9
+
10
+ /** Supported migration sources. */
11
+ export type MigrateSource = "claude-code" | "codex" | "opencode";
12
+
13
+ export const MIGRATE_SOURCES: readonly MigrateSource[] = ["claude-code", "codex", "opencode"];
14
+
15
+ /** Canonical, deterministic ordering used when expanding `--from all` / repeated `--from`. */
16
+ export const CANONICAL_SOURCE_ORDER: readonly MigrateSource[] = MIGRATE_SOURCES;
17
+
18
+ /** What kind of thing an action/coverage row is about. */
19
+ export type MigrateItemType = "mcp" | "skill" | "source";
20
+
21
+ /**
22
+ * Per-item outcome taxonomy.
23
+ *
24
+ * `skipped_*` outcomes are non-fatal (exit 0). Any `failed_*` outcome sets
25
+ * `ok=false` and a non-zero process exit code.
26
+ */
27
+ export type MigrationStatus =
28
+ | "imported"
29
+ | "updated"
30
+ | "skipped_exists"
31
+ | "skipped_absent_source"
32
+ | "skipped_unmappable"
33
+ | "failed_invalid_source"
34
+ | "failed_invalid_destination"
35
+ | "failed_io";
36
+
37
+ export const MIGRATION_STATUSES: readonly MigrationStatus[] = [
38
+ "imported",
39
+ "updated",
40
+ "skipped_exists",
41
+ "skipped_absent_source",
42
+ "skipped_unmappable",
43
+ "failed_invalid_source",
44
+ "failed_invalid_destination",
45
+ "failed_io",
46
+ ];
47
+
48
+ /** Statuses that represent a hard failure (drive `ok=false` + non-zero exit). */
49
+ export const FAILURE_STATUSES: ReadonlySet<MigrationStatus> = new Set<MigrationStatus>([
50
+ "failed_invalid_source",
51
+ "failed_invalid_destination",
52
+ "failed_io",
53
+ ]);
54
+
55
+ export function isFailureStatus(status: MigrationStatus): boolean {
56
+ return FAILURE_STATUSES.has(status);
57
+ }
58
+
59
+ /** Operation the planner decided for an item. */
60
+ export type MigrateOperation = "create" | "update" | "skip" | "fail";
61
+
62
+ /** A raw MCP server candidate parsed from a source, before mapping/destination planning. */
63
+ export interface McpCandidate {
64
+ source: MigrateSource;
65
+ name: string;
66
+ /** The raw, unmapped server entry from the source config (mapped by the planner). */
67
+ raw: unknown;
68
+ }
69
+
70
+ /** A raw skill candidate parsed from a source, before normalization/destination planning. */
71
+ export interface SkillCandidate {
72
+ source: MigrateSource;
73
+ /** Slug used as the destination directory and effective loaded name. */
74
+ slug: string;
75
+ /** Full SKILL.md content (frontmatter already normalized so loaded name == slug). */
76
+ content: string;
77
+ warnings: string[];
78
+ }
79
+
80
+ /**
81
+ * Source-level diagnostic for a single source/type pair (e.g. "codex mcp config
82
+ * was malformed"). Distinct from per-item actions so absent/unreadable sources
83
+ * are reported once instead of per item.
84
+ */
85
+ export interface SourceDiagnostic {
86
+ source: MigrateSource;
87
+ type: Exclude<MigrateItemType, "source"> | "source";
88
+ status: Extract<MigrationStatus, "skipped_absent_source" | "failed_invalid_source" | "failed_io">;
89
+ message: string;
90
+ }
91
+
92
+ /** Normalized candidates + diagnostics returned by an adapter. */
93
+ export interface AdapterResult {
94
+ mcpCandidates: McpCandidate[];
95
+ skillCandidates: SkillCandidate[];
96
+ diagnostics: SourceDiagnostic[];
97
+ }
98
+
99
+ /** A single planned action consumed identically by dry-run and live execution. */
100
+ export interface MigrateAction {
101
+ source: MigrateSource;
102
+ type: MigrateItemType;
103
+ name?: string;
104
+ /** For skills: the effective GJC-loaded name (== slug). */
105
+ effectiveName?: string;
106
+ /** Absolute destination path (mcp.json for MCP, <skillsDir>/<slug>/SKILL.md for skills). */
107
+ destination?: string;
108
+ operation: MigrateOperation;
109
+ status: MigrationStatus;
110
+ reason?: string;
111
+ warnings?: string[];
112
+ /** Resolved payload the executor needs; never serialized to the report. */
113
+ mcp?: { config: MCPServerConfig; force: boolean };
114
+ skill?: { content: string };
115
+ }
116
+
117
+ export interface MigrateWarning {
118
+ source: MigrateSource;
119
+ type: string;
120
+ name?: string;
121
+ message: string;
122
+ }
123
+
124
+ export type StatusCounts = Record<MigrationStatus, number>;
125
+
126
+ export interface MigrateDestinations {
127
+ mcpConfigPath: string;
128
+ skillsDir: string;
129
+ }
130
+
131
+ /** The full machine-readable report emitted with `--json`. */
132
+ export interface MigrateReport {
133
+ ok: boolean;
134
+ dryRun: boolean;
135
+ project: boolean;
136
+ force: boolean;
137
+ sources: MigrateSource[];
138
+ destinations: MigrateDestinations;
139
+ summary: {
140
+ total: StatusCounts;
141
+ byType: { mcp: StatusCounts; skill: StatusCounts; source: StatusCounts };
142
+ bySource: Record<MigrateSource, StatusCounts>;
143
+ };
144
+ actions: Array<{
145
+ source: MigrateSource;
146
+ type: MigrateItemType;
147
+ name?: string;
148
+ effectiveName?: string;
149
+ destination?: string;
150
+ operation: MigrateOperation;
151
+ status: MigrationStatus;
152
+ reason?: string;
153
+ warnings?: string[];
154
+ }>;
155
+ warnings: MigrateWarning[];
156
+ }
157
+
158
+ /** Create a zeroed status-count record. */
159
+ export function emptyStatusCounts(): StatusCounts {
160
+ const counts = {} as StatusCounts;
161
+ for (const status of MIGRATION_STATUSES) counts[status] = 0;
162
+ return counts;
163
+ }
@@ -1,5 +1,5 @@
1
- import * as path from "node:path";
2
1
  import type { ExtensionUIContext } from "../../extensibility/extensions";
2
+ import { workflowGatePath } from "../../gjc-runtime/session-layout";
3
3
  import type { AgentSession } from "../../session/agent-session";
4
4
  import type { ClientBridgePermissionOutcome } from "../../session/client-bridge";
5
5
  import type { RpcCommand, RpcResponse, RpcWorkflowGateResponse } from "../rpc/rpc-types";
@@ -611,7 +611,7 @@ export async function runBridgeMode(
611
611
  });
612
612
  };
613
613
  const gateStore = new FileGateStore(
614
- path.join(session.sessionManager.getCwd(), ".gjc", "state", "workflow-gates", `${session.sessionId}.json`),
614
+ workflowGatePath(session.sessionManager.getCwd(), session.sessionId, session.sessionId),
615
615
  );
616
616
  const unattendedControlPlane = new UnattendedSessionControlPlane({
617
617
  runId: session.sessionId,