@gajae-code/coding-agent 0.2.1 → 0.2.3

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 (153) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/session.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +2 -2
  8. package/dist/types/config/models-config-schema.d.ts +17 -9
  9. package/dist/types/config/settings-schema.d.ts +37 -24
  10. package/dist/types/discovery/helpers.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
  13. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  16. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  18. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  19. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  20. package/dist/types/goals/runtime.d.ts +3 -9
  21. package/dist/types/goals/state.d.ts +3 -6
  22. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  23. package/dist/types/hooks/skill-state.d.ts +5 -0
  24. package/dist/types/memories/index.d.ts +1 -1
  25. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  26. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +0 -2
  28. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  29. package/dist/types/modes/components/status-line.d.ts +0 -3
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +1 -12
  32. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  33. package/dist/types/modes/theme/theme.d.ts +1 -2
  34. package/dist/types/modes/types.d.ts +1 -7
  35. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  36. package/dist/types/sdk.d.ts +6 -2
  37. package/dist/types/session/agent-session.d.ts +47 -1
  38. package/dist/types/session/contribution-prep.d.ts +47 -0
  39. package/dist/types/session/session-manager.d.ts +3 -0
  40. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  41. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  42. package/dist/types/skill-state/active-state.d.ts +30 -1
  43. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  44. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  45. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  46. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  47. package/dist/types/task/executor.d.ts +2 -0
  48. package/dist/types/task/types.d.ts +11 -0
  49. package/dist/types/tools/index.d.ts +20 -1
  50. package/dist/types/tools/skill.d.ts +47 -0
  51. package/dist/types/utils/changelog.d.ts +18 -2
  52. package/package.json +7 -7
  53. package/src/cli/args.ts +3 -2
  54. package/src/cli/setup-cli.ts +26 -12
  55. package/src/cli.ts +7 -1
  56. package/src/commands/contribution-prep.ts +41 -0
  57. package/src/commands/deep-interview.ts +30 -23
  58. package/src/commands/launch.ts +10 -1
  59. package/src/commands/ralplan.ts +10 -22
  60. package/src/commands/session.ts +150 -0
  61. package/src/commands/setup.ts +2 -0
  62. package/src/commands/state.ts +15 -4
  63. package/src/commands/team.ts +23 -3
  64. package/src/config/model-registry.ts +10 -2
  65. package/src/config/models-config-schema.ts +120 -102
  66. package/src/config/settings-schema.ts +42 -25
  67. package/src/config.ts +1 -1
  68. package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
  69. package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
  70. package/src/defaults/gjc/skills/team/SKILL.md +39 -7
  71. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
  72. package/src/discovery/helpers.ts +24 -1
  73. package/src/eval/py/prelude.py +1 -1
  74. package/src/extensibility/extensions/types.ts +6 -0
  75. package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
  76. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  77. package/src/gjc-runtime/launch-tmux.ts +83 -43
  78. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  79. package/src/gjc-runtime/state-runtime.ts +731 -0
  80. package/src/gjc-runtime/team-runtime.ts +708 -52
  81. package/src/gjc-runtime/tmux-common.ts +119 -0
  82. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  83. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  84. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  85. package/src/goals/runtime.ts +38 -144
  86. package/src/goals/state.ts +36 -7
  87. package/src/goals/tools/goal-tool.ts +15 -172
  88. package/src/hooks/skill-state.ts +39 -18
  89. package/src/internal-urls/docs-index.generated.ts +5 -4
  90. package/src/internal-urls/memory-protocol.ts +3 -2
  91. package/src/main.ts +2 -3
  92. package/src/memories/index.ts +2 -1
  93. package/src/memory-backend/local-backend.ts +14 -6
  94. package/src/modes/components/hook-selector.ts +156 -1
  95. package/src/modes/components/settings-selector.ts +5 -12
  96. package/src/modes/components/skill-hud/render.ts +4 -0
  97. package/src/modes/components/status-line/segments.ts +5 -16
  98. package/src/modes/components/status-line/types.ts +0 -3
  99. package/src/modes/components/status-line.ts +0 -6
  100. package/src/modes/controllers/command-controller.ts +27 -4
  101. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  102. package/src/modes/controllers/input-controller.ts +0 -15
  103. package/src/modes/controllers/selector-controller.ts +4 -11
  104. package/src/modes/interactive-mode.ts +18 -219
  105. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  106. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  107. package/src/modes/theme/theme.ts +0 -6
  108. package/src/modes/types.ts +1 -7
  109. package/src/modes/utils/context-usage.ts +66 -17
  110. package/src/prompts/agents/architect.md +3 -0
  111. package/src/prompts/agents/executor.md +2 -0
  112. package/src/prompts/agents/frontmatter.md +1 -0
  113. package/src/prompts/goals/goal-continuation.md +1 -4
  114. package/src/prompts/goals/goal-mode-active.md +3 -5
  115. package/src/prompts/system/subagent-system-prompt.md +6 -0
  116. package/src/prompts/system/system-prompt.md +5 -7
  117. package/src/prompts/tools/goal.md +4 -4
  118. package/src/prompts/tools/skill.md +28 -0
  119. package/src/prompts/tools/task.md +3 -0
  120. package/src/sdk.ts +51 -11
  121. package/src/session/agent-session.ts +222 -21
  122. package/src/session/contribution-prep.ts +320 -0
  123. package/src/session/session-manager.ts +9 -1
  124. package/src/setup/model-onboarding-guidance.ts +6 -3
  125. package/src/setup/provider-onboarding.ts +177 -16
  126. package/src/skill-state/active-state.ts +188 -25
  127. package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
  128. package/src/skill-state/initial-phase.ts +17 -0
  129. package/src/skill-state/workflow-hud.ts +23 -5
  130. package/src/skill-state/workflow-state-contract.ts +121 -0
  131. package/src/slash-commands/builtin-registry.ts +75 -25
  132. package/src/slash-commands/helpers/context-report.ts +123 -13
  133. package/src/task/agents.ts +1 -0
  134. package/src/task/commands.ts +1 -5
  135. package/src/task/executor.ts +9 -1
  136. package/src/task/index.ts +91 -4
  137. package/src/task/types.ts +6 -0
  138. package/src/tools/ask.ts +2 -0
  139. package/src/tools/gh.ts +212 -2
  140. package/src/tools/index.ts +25 -6
  141. package/src/tools/skill.ts +153 -0
  142. package/src/utils/changelog.ts +67 -44
  143. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  144. package/dist/types/commands/question.d.ts +0 -7
  145. package/dist/types/modes/loop-limit.d.ts +0 -22
  146. package/src/commands/gjc-runtime-bridge.ts +0 -227
  147. package/src/commands/question.ts +0 -12
  148. package/src/modes/loop-limit.ts +0 -140
  149. package/src/prompts/commands/orchestrate.md +0 -49
  150. package/src/prompts/goals/goal-budget-limit.md +0 -16
  151. package/src/prompts/tools/create-goal.md +0 -3
  152. package/src/prompts/tools/get-goal.md +0 -3
  153. package/src/prompts/tools/update-goal.md +0 -3
@@ -1,12 +1,30 @@
1
1
  import * as path from "node:path";
2
2
  import type { Args } from "../cli/args";
3
+ import {
4
+ buildGjcTmuxProfileCommands,
5
+ buildGjcTmuxSessionName,
6
+ buildGjcTmuxSessionSlug,
7
+ GJC_DEFAULT_TMUX_SESSION,
8
+ GJC_TMUX_COMMAND_ENV,
9
+ GJC_TMUX_MOUSE_ENV,
10
+ GJC_TMUX_PROFILE_ENV,
11
+ GJC_TMUX_SESSION_PREFIX,
12
+ type GjcTmuxProfileCommand,
13
+ resolveGjcTmuxCommand,
14
+ } from "./tmux-common";
15
+ import { findGjcTmuxSessionByBranch } from "./tmux-sessions";
16
+
17
+ export {
18
+ buildGjcTmuxProfileCommands,
19
+ GJC_DEFAULT_TMUX_SESSION,
20
+ GJC_TMUX_COMMAND_ENV,
21
+ GJC_TMUX_MOUSE_ENV,
22
+ GJC_TMUX_PROFILE_ENV,
23
+ GJC_TMUX_SESSION_PREFIX,
24
+ };
3
25
 
4
- export const GJC_DEFAULT_TMUX_SESSION = "gajae_code";
5
26
  export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
6
27
  export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
7
- export const GJC_TMUX_COMMAND_ENV = "GJC_TMUX_COMMAND";
8
- export const GJC_TMUX_PROFILE_ENV = "GJC_TMUX_PROFILE";
9
- export const GJC_TMUX_MOUSE_ENV = "GJC_MOUSE";
10
28
 
11
29
  type LaunchPolicy = "direct" | "tmux";
12
30
 
@@ -26,6 +44,10 @@ export interface TmuxLaunchContext {
26
44
  tty?: TtyState;
27
45
  spawnSync?: TmuxSpawnSync;
28
46
  tmuxAvailable?: boolean;
47
+ worktreeBranch?: string | null;
48
+ currentBranch?: string | null;
49
+ existingBranchSessionName?: string | null;
50
+ project?: string | null;
29
51
  }
30
52
 
31
53
  export interface TmuxSpawnResult {
@@ -50,12 +72,9 @@ export interface TmuxLaunchPlan {
50
72
  cwd: string;
51
73
  innerCommand: string;
52
74
  newSessionArgs: string[];
53
- attachSessionArgs: string[];
54
- }
55
-
56
- export interface GjcTmuxProfileCommand {
57
- description: string;
58
- args: string[];
75
+ branch?: string | null;
76
+ attachSessionName?: string;
77
+ project?: string | null;
59
78
  }
60
79
 
61
80
  export interface GjcTmuxProfileResult {
@@ -70,6 +89,9 @@ export interface GjcTmuxProfileContext {
70
89
  cwd?: string;
71
90
  env?: NodeJS.ProcessEnv;
72
91
  spawnSync?: TmuxSpawnSync;
92
+ branch?: string | null;
93
+ branchSlug?: string | null;
94
+ project?: string | null;
73
95
  }
74
96
 
75
97
  interface CommandResolutionContext {
@@ -103,35 +125,14 @@ function shellQuote(value: string): string {
103
125
  return `'${value.replace(/'/g, `'\\''`)}'`;
104
126
  }
105
127
 
106
- function envDisabled(value: string | undefined): boolean {
107
- const normalized = value?.trim().toLowerCase();
108
- return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
109
- }
110
-
111
- export function buildGjcTmuxProfileCommands(
112
- target: string,
113
- env: NodeJS.ProcessEnv = process.env,
114
- ): GjcTmuxProfileCommand[] {
115
- if (envDisabled(env[GJC_TMUX_PROFILE_ENV])) return [];
116
- const commands: GjcTmuxProfileCommand[] = [
117
- { description: "mark GJC tmux ownership", args: ["set-option", "-t", target, "@gjc-profile", "1"] },
118
- { description: "enable tmux clipboard integration", args: ["set-option", "-t", target, "set-clipboard", "on"] },
119
- {
120
- description: "make copy-mode selection readable",
121
- args: ["set-window-option", "-t", target, "mode-style", "fg=colour231,bg=colour60"],
122
- },
123
- ];
124
- if (!envDisabled(env[GJC_TMUX_MOUSE_ENV]))
125
- commands.unshift({
126
- description: "enable tmux mouse scrolling",
127
- args: ["set-option", "-t", target, "mouse", "on"],
128
- });
129
- return commands;
130
- }
131
-
132
128
  export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
133
129
  const env = context.env ?? process.env;
134
- const commands = buildGjcTmuxProfileCommands(context.target, env);
130
+ const branchSlug = context.branch ? buildGjcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
131
+ const commands = buildGjcTmuxProfileCommands(context.target, env, {
132
+ branch: context.branch ?? null,
133
+ branchSlug,
134
+ project: context.project ?? null,
135
+ });
135
136
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
136
137
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
137
138
  const cwd = context.cwd ?? process.cwd();
@@ -160,6 +161,25 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
160
161
  return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1 ${quoted}`;
161
162
  }
162
163
 
164
+ function readCurrentBranch(cwd: string): string | null {
165
+ try {
166
+ const result = Bun.spawnSync(["git", "symbolic-ref", "--quiet", "--short", "HEAD"], {
167
+ cwd,
168
+ stdout: "pipe",
169
+ stderr: "ignore",
170
+ });
171
+ if (result.exitCode !== 0) return null;
172
+ const branch = result.stdout.toString().trim();
173
+ return branch || null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
180
+ spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
181
+ }
182
+
163
183
  export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
164
184
  const env = context.env ?? process.env;
165
185
  const policy = parseLaunchPolicy(env);
@@ -171,10 +191,18 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
171
191
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
172
192
 
173
193
  const cwd = context.cwd ?? process.cwd();
174
- const sessionName = env.GJC_TMUX_SESSION?.trim() || GJC_DEFAULT_TMUX_SESSION;
175
- const tmuxCommand = env[GJC_TMUX_COMMAND_ENV]?.trim() || "tmux";
194
+ const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
195
+ const project = context.project ?? cwd;
196
+ const sessionName = buildGjcTmuxSessionName(env, { branch });
197
+ const tmuxCommand = resolveGjcTmuxCommand(env);
176
198
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
177
199
  if (!tmuxAvailable) return undefined;
200
+ const existingBranchSessionName =
201
+ "existingBranchSessionName" in context
202
+ ? (context.existingBranchSessionName ?? undefined)
203
+ : context.worktreeBranch
204
+ ? findGjcTmuxSessionByBranch(context.worktreeBranch, env, project)?.name
205
+ : undefined;
178
206
  const innerCommand = buildInnerCommand(
179
207
  {
180
208
  cwd,
@@ -189,7 +217,9 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
189
217
  cwd,
190
218
  innerCommand,
191
219
  newSessionArgs: ["new-session", "-d", "-s", sessionName, "-c", cwd, innerCommand],
192
- attachSessionArgs: ["attach-session", "-t", sessionName],
220
+ branch,
221
+ project,
222
+ attachSessionName: existingBranchSessionName,
193
223
  };
194
224
  }
195
225
 
@@ -217,17 +247,27 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
217
247
  stdout: "inherit",
218
248
  stderr: "inherit",
219
249
  };
250
+ if (plan.attachSessionName) {
251
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
252
+ return attached.exitCode === 0;
253
+ }
220
254
  const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
221
255
  if (created.exitCode === 0) {
222
- applyGjcTmuxProfile({
256
+ const profile = applyGjcTmuxProfile({
223
257
  tmuxCommand: plan.tmuxCommand,
224
258
  target: plan.sessionName,
225
259
  cwd: plan.cwd,
226
260
  env,
227
261
  spawnSync,
262
+ branch: plan.branch,
263
+ project: plan.project,
228
264
  });
265
+ if (profile.failures.length > 0) {
266
+ cleanupCreatedTmuxSession(plan, spawnSync, options);
267
+ return false;
268
+ }
229
269
  }
230
- const attached = spawnSync(plan.tmuxCommand, plan.attachSessionArgs, options);
231
- if (created.exitCode === 0) return attached.exitCode === 0;
270
+ if (created.exitCode !== 0) return false;
271
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
232
272
  return attached.exitCode === 0;
233
273
  }
@@ -0,0 +1,460 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { syncSkillActiveState } from "../skill-state/active-state";
5
+ import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
6
+
7
+ /**
8
+ * Native implementation of `gjc ralplan`.
9
+ *
10
+ * Two invocation shapes are handled natively:
11
+ *
12
+ * 1. **Consensus handoff**: `gjc ralplan [--interactive] [--deliberate] [--architect <kind>]
13
+ * [--critic <kind>] [--session-id <id>] "<task>"` validates the documented flag surface,
14
+ * seeds `.gjc/state/ralplan-state.json`, and updates the shared HUD rail via
15
+ * `syncSkillActiveState`. The CLI never *runs* the Planner / Architect / Critic loop itself —
16
+ * that lives in the bundled `/skill:ralplan` skill — but it accepts every documented flag so
17
+ * scripted users see a useful response and the active run is visible to the TUI.
18
+ *
19
+ * 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N> --artifact
20
+ * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
21
+ * / Critic / revision / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
22
+ * an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
23
+ * the HUD chip to reflect the latest persisted stage.
24
+ */
25
+
26
+ export interface RalplanCommandResult {
27
+ status: number;
28
+ stdout?: string;
29
+ stderr?: string;
30
+ }
31
+
32
+ const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "adr", "final"] as const;
33
+ type RalplanStage = (typeof KNOWN_STAGES)[number];
34
+
35
+ const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
36
+ const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
37
+
38
+ const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
39
+
40
+ class RalplanCommandError extends Error {
41
+ constructor(
42
+ public readonly exitStatus: number,
43
+ message: string,
44
+ ) {
45
+ super(message);
46
+ this.name = "RalplanCommandError";
47
+ }
48
+ }
49
+
50
+ const VALUE_FLAGS = new Set([
51
+ "--stage",
52
+ "--stage_n",
53
+ "--artifact",
54
+ "--run-id",
55
+ "--session-id",
56
+ "--architect",
57
+ "--critic",
58
+ ]);
59
+
60
+ function flagValue(args: readonly string[], flag: string): string | undefined {
61
+ const index = args.indexOf(flag);
62
+ if (index < 0) return undefined;
63
+ return args[index + 1];
64
+ }
65
+
66
+ function hasFlag(args: readonly string[], flag: string): boolean {
67
+ return args.includes(flag);
68
+ }
69
+
70
+ export function isRalplanArtifactWriteInvocation(args: readonly string[]): boolean {
71
+ return hasFlag(args, "--write");
72
+ }
73
+
74
+ function assertSafePathComponent(value: string, label: string): void {
75
+ if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
76
+ throw new RalplanCommandError(2, `invalid path component for --${label}: ${value}`);
77
+ }
78
+ }
79
+
80
+ function assertKnownStage(stage: string): asserts stage is RalplanStage {
81
+ if (!(KNOWN_STAGES as readonly string[]).includes(stage)) {
82
+ throw new RalplanCommandError(2, `unknown --stage: ${stage}. Expected one of: ${KNOWN_STAGES.join(", ")}.`);
83
+ }
84
+ }
85
+
86
+ function parseStageN(raw: string | undefined): number {
87
+ if (!raw) throw new RalplanCommandError(2, "--stage_n is required");
88
+ if (!/^[1-9][0-9]{0,2}$/.test(raw)) {
89
+ throw new RalplanCommandError(2, `invalid --stage_n: ${raw}. Expected integer 1..999.`);
90
+ }
91
+ const value = Number.parseInt(raw, 10);
92
+ if (value < 1 || value > 999) {
93
+ throw new RalplanCommandError(2, `invalid --stage_n: ${raw}. Expected integer 1..999.`);
94
+ }
95
+ return value;
96
+ }
97
+
98
+ function pad2(value: number): string {
99
+ return value.toString().padStart(2, "0");
100
+ }
101
+
102
+ function defaultRunId(now: Date = new Date()): string {
103
+ const yyyy = now.getUTCFullYear().toString().padStart(4, "0");
104
+ const mm = (now.getUTCMonth() + 1).toString().padStart(2, "0");
105
+ const dd = now.getUTCDate().toString().padStart(2, "0");
106
+ const hh = now.getUTCHours().toString().padStart(2, "0");
107
+ const min = now.getUTCMinutes().toString().padStart(2, "0");
108
+ const suffix = randomBytes(2).toString("hex");
109
+ return `${yyyy}-${mm}-${dd}-${hh}${min}-${suffix}`;
110
+ }
111
+
112
+ async function resolveArtifactContent(rawArtifact: string, cwd: string): Promise<string> {
113
+ const candidate = path.isAbsolute(rawArtifact) ? rawArtifact : path.resolve(cwd, rawArtifact);
114
+ try {
115
+ const stat = await fs.stat(candidate);
116
+ if (stat.isFile()) return await fs.readFile(candidate, "utf-8");
117
+ } catch (error) {
118
+ const err = error as NodeJS.ErrnoException;
119
+ if (err.code !== "ENOENT" && err.code !== "ENOTDIR") {
120
+ throw new RalplanCommandError(2, `failed to read --artifact ${candidate}: ${err.message}`);
121
+ }
122
+ }
123
+ return rawArtifact;
124
+ }
125
+
126
+ /* ------------------------------ artifact write ------------------------------ */
127
+
128
+ interface ResolvedArtifactArgs {
129
+ stage: RalplanStage;
130
+ stageN: number;
131
+ runId: string;
132
+ artifact: string;
133
+ sessionId: string | undefined;
134
+ json: boolean;
135
+ }
136
+
137
+ function ralplanStatePath(cwd: string, sessionId: string | undefined): string {
138
+ const stateDir = sessionId
139
+ ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(sessionId))
140
+ : path.join(cwd, ".gjc", "state");
141
+ return path.join(stateDir, "ralplan-state.json");
142
+ }
143
+
144
+ async function readActiveRunId(cwd: string, sessionId: string | undefined): Promise<string | undefined> {
145
+ try {
146
+ const raw = await fs.readFile(ralplanStatePath(cwd, sessionId), "utf-8");
147
+ const parsed = JSON.parse(raw) as { run_id?: unknown };
148
+ const candidate = typeof parsed.run_id === "string" ? parsed.run_id.trim() : "";
149
+ if (!candidate) return undefined;
150
+ assertSafePathComponent(candidate, "run-id");
151
+ return candidate;
152
+ } catch {
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ async function persistActiveRunId(cwd: string, sessionId: string | undefined, runId: string): Promise<void> {
158
+ const statePath = ralplanStatePath(cwd, sessionId);
159
+ let existing: Record<string, unknown> = {};
160
+ try {
161
+ const raw = await fs.readFile(statePath, "utf-8");
162
+ const parsed = JSON.parse(raw);
163
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
164
+ existing = parsed as Record<string, unknown>;
165
+ }
166
+ } catch {
167
+ // fresh receipt; fall through to create
168
+ }
169
+ if (existing.run_id === runId) return;
170
+ existing.run_id = runId;
171
+ if (typeof existing.skill !== "string") existing.skill = "ralplan";
172
+ if (typeof existing.active !== "boolean") existing.active = true;
173
+ existing.updated_at = new Date().toISOString();
174
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
175
+ await fs.writeFile(statePath, `${JSON.stringify(existing, null, 2)}\n`);
176
+ }
177
+
178
+ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
179
+ const stage = flagValue(args, "--stage");
180
+ if (!stage) throw new RalplanCommandError(2, "--stage is required for ralplan --write");
181
+ assertKnownStage(stage);
182
+
183
+ const stageN = parseStageN(flagValue(args, "--stage_n"));
184
+
185
+ const rawArtifact = flagValue(args, "--artifact");
186
+ if (rawArtifact === undefined || rawArtifact === "") {
187
+ throw new RalplanCommandError(2, "--artifact is required for ralplan --write");
188
+ }
189
+
190
+ const sessionIdRaw = flagValue(args, "--session-id")?.trim();
191
+ const sessionId = sessionIdRaw || undefined;
192
+ if (sessionId) assertSafePathComponent(sessionId, "session-id");
193
+
194
+ // Precedence for run_id:
195
+ // 1. explicit --run-id flag
196
+ // 2. existing run_id field in .gjc/state[/sessions/<id>]/ralplan-state.json
197
+ // 3. explicit --session-id flag (use as run id)
198
+ // 4. freshly generated default run id
199
+ const explicitRunId = flagValue(args, "--run-id")?.trim();
200
+ const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
201
+ assertSafePathComponent(runId, "run-id");
202
+ // Persist the active run id so later writes in the same loop land in the same directory.
203
+ await persistActiveRunId(cwd, sessionId, runId);
204
+
205
+ const artifact = await resolveArtifactContent(rawArtifact, cwd);
206
+ return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
207
+ }
208
+
209
+ interface PersistedArtifact {
210
+ runId: string;
211
+ path: string;
212
+ stage: RalplanStage;
213
+ stageN: number;
214
+ sha256: string;
215
+ createdAt: string;
216
+ pendingApprovalPath?: string;
217
+ }
218
+
219
+ async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Promise<PersistedArtifact> {
220
+ const runDir = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId);
221
+ await fs.mkdir(runDir, { recursive: true });
222
+ const fileName = `stage-${pad2(resolved.stageN)}-${resolved.stage}.md`;
223
+ const filePath = path.join(runDir, fileName);
224
+ const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
225
+ await fs.writeFile(filePath, content);
226
+
227
+ const sha256 = createHash("sha256").update(content).digest("hex");
228
+ const createdAt = new Date().toISOString();
229
+ const indexLine = `${JSON.stringify({
230
+ stage: resolved.stage,
231
+ stage_n: resolved.stageN,
232
+ path: filePath,
233
+ created_at: createdAt,
234
+ sha256,
235
+ })}\n`;
236
+ await fs.appendFile(path.join(runDir, "index.jsonl"), indexLine);
237
+
238
+ let pendingApprovalPath: string | undefined;
239
+ if (resolved.stage === "final") {
240
+ pendingApprovalPath = path.join(runDir, "pending-approval.md");
241
+ await fs.writeFile(pendingApprovalPath, content);
242
+ }
243
+
244
+ return {
245
+ runId: resolved.runId,
246
+ path: filePath,
247
+ stage: resolved.stage,
248
+ stageN: resolved.stageN,
249
+ sha256,
250
+ createdAt,
251
+ pendingApprovalPath,
252
+ };
253
+ }
254
+
255
+ async function syncRalplanHud(options: {
256
+ cwd: string;
257
+ sessionId?: string;
258
+ stage: string;
259
+ pendingApproval: boolean;
260
+ iteration?: number;
261
+ latestSummary?: string;
262
+ }): Promise<void> {
263
+ try {
264
+ await syncSkillActiveState({
265
+ cwd: options.cwd,
266
+ skill: "ralplan",
267
+ active: !options.pendingApproval || options.stage === "final",
268
+ phase: options.stage,
269
+ sessionId: options.sessionId,
270
+ source: "gjc-ralplan-native",
271
+ hud: buildRalplanHudSummary({
272
+ stage: options.stage,
273
+ iteration: options.iteration,
274
+ pendingApproval: options.pendingApproval,
275
+ latestSummary: options.latestSummary,
276
+ updatedAt: new Date().toISOString(),
277
+ }),
278
+ });
279
+ } catch {
280
+ // HUD sync is best-effort and must not change command semantics.
281
+ }
282
+ }
283
+
284
+ async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
285
+ const resolved = await resolveArtifactArgs(args, cwd);
286
+ const persisted = await persistArtifact(resolved, cwd);
287
+ await syncRalplanHud({
288
+ cwd,
289
+ sessionId: resolved.sessionId,
290
+ stage: persisted.stage,
291
+ pendingApproval: persisted.stage === "final",
292
+ iteration: persisted.stageN,
293
+ latestSummary: `persisted ${persisted.stage} stage ${persisted.stageN}`,
294
+ });
295
+ const payload: Record<string, unknown> = {
296
+ run_id: persisted.runId,
297
+ path: persisted.path,
298
+ stage: persisted.stage,
299
+ stage_n: persisted.stageN,
300
+ sha256: persisted.sha256,
301
+ created_at: persisted.createdAt,
302
+ };
303
+ if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
304
+ const stdout = resolved.json
305
+ ? `${JSON.stringify(payload, null, 2)}\n`
306
+ : `Persisted ralplan ${persisted.stage} stage ${persisted.stageN} at ${persisted.path}.\n`;
307
+ return { status: 0, stdout };
308
+ }
309
+
310
+ /* -------------------------------- handoff -------------------------------- */
311
+
312
+ interface ConsensusHandoffArgs {
313
+ interactive: boolean;
314
+ deliberate: boolean;
315
+ architectKind?: string;
316
+ criticKind?: string;
317
+ sessionId?: string;
318
+ task: string;
319
+ json: boolean;
320
+ }
321
+
322
+ function extractPositionalTask(args: readonly string[]): string {
323
+ const parts: string[] = [];
324
+ let skipNext = false;
325
+ for (const arg of args) {
326
+ if (skipNext) {
327
+ skipNext = false;
328
+ continue;
329
+ }
330
+ if (VALUE_FLAGS.has(arg)) {
331
+ skipNext = true;
332
+ continue;
333
+ }
334
+ if (arg === "--interactive" || arg === "--deliberate" || arg === "--write" || arg === "--json") continue;
335
+ if (arg.startsWith("-")) {
336
+ throw new RalplanCommandError(2, `unknown flag for gjc ralplan: ${arg}`);
337
+ }
338
+ parts.push(arg);
339
+ }
340
+ return parts.join(" ").trim();
341
+ }
342
+
343
+ function resolveConsensusArgs(args: readonly string[]): ConsensusHandoffArgs {
344
+ const architectKind = flagValue(args, "--architect")?.trim() || undefined;
345
+ if (architectKind && !KNOWN_ARCHITECT_KINDS.has(architectKind)) {
346
+ throw new RalplanCommandError(
347
+ 2,
348
+ `unknown --architect kind: ${architectKind}. Expected one of: ${[...KNOWN_ARCHITECT_KINDS].join(", ")}.`,
349
+ );
350
+ }
351
+ const criticKind = flagValue(args, "--critic")?.trim() || undefined;
352
+ if (criticKind && !KNOWN_CRITIC_KINDS.has(criticKind)) {
353
+ throw new RalplanCommandError(
354
+ 2,
355
+ `unknown --critic kind: ${criticKind}. Expected one of: ${[...KNOWN_CRITIC_KINDS].join(", ")}.`,
356
+ );
357
+ }
358
+ const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
359
+ if (sessionId) assertSafePathComponent(sessionId, "session-id");
360
+ const task = extractPositionalTask(args);
361
+ return {
362
+ interactive: hasFlag(args, "--interactive"),
363
+ deliberate: hasFlag(args, "--deliberate"),
364
+ architectKind,
365
+ criticKind,
366
+ sessionId,
367
+ task,
368
+ json: hasFlag(args, "--json"),
369
+ };
370
+ }
371
+
372
+ function encodeSessionSegment(value: string): string {
373
+ return encodeURIComponent(value).replaceAll(".", "%2E");
374
+ }
375
+
376
+ async function seedRalplanState(
377
+ cwd: string,
378
+ resolved: ConsensusHandoffArgs,
379
+ ): Promise<{ statePath: string; runId: string }> {
380
+ const stateDir = resolved.sessionId
381
+ ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
382
+ : path.join(cwd, ".gjc", "state");
383
+ await fs.mkdir(stateDir, { recursive: true });
384
+ const statePath = path.join(stateDir, "ralplan-state.json");
385
+ // Reuse an existing run id when present so a re-invocation of `gjc ralplan "task"` doesn't
386
+ // orphan in-progress artifacts under a fresh run id.
387
+ const existingRunId = await readActiveRunId(cwd, resolved.sessionId);
388
+ const runId = existingRunId ?? resolved.sessionId ?? defaultRunId();
389
+ assertSafePathComponent(runId, "run-id");
390
+ const now = new Date().toISOString();
391
+ const payload: Record<string, unknown> = {
392
+ active: true,
393
+ current_phase: "planner",
394
+ skill: "ralplan",
395
+ mode: resolved.deliberate ? "deliberate" : "short",
396
+ interactive: resolved.interactive,
397
+ task: resolved.task,
398
+ run_id: runId,
399
+ updated_at: now,
400
+ };
401
+ if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
402
+ if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
403
+ if (resolved.sessionId) payload.session_id = resolved.sessionId;
404
+ await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
405
+ return { statePath, runId };
406
+ }
407
+
408
+ async function handleConsensusHandoff(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
409
+ const resolved = resolveConsensusArgs(args);
410
+ if (!resolved.task) {
411
+ throw new RalplanCommandError(2, 'gjc ralplan requires a task description, e.g. `gjc ralplan "<task>"`.');
412
+ }
413
+ const { statePath, runId } = await seedRalplanState(cwd, resolved);
414
+ const mode = resolved.deliberate ? "deliberate" : "short";
415
+ await syncRalplanHud({
416
+ cwd,
417
+ sessionId: resolved.sessionId,
418
+ stage: "planner",
419
+ pendingApproval: false,
420
+ iteration: 1,
421
+ latestSummary: `${mode} run · ${resolved.interactive ? "interactive" : "automated"}`,
422
+ });
423
+
424
+ const summary = {
425
+ skill: "ralplan",
426
+ mode,
427
+ interactive: resolved.interactive,
428
+ architect: resolved.architectKind ?? "default",
429
+ critic: resolved.criticKind ?? "default",
430
+ task: resolved.task,
431
+ state_path: statePath,
432
+ run_id: runId,
433
+ handoff: "Run `/skill:ralplan` inside the GJC agent to drive the Planner / Architect / Critic consensus loop.",
434
+ };
435
+ const stdout = resolved.json
436
+ ? `${JSON.stringify(summary, null, 2)}\n`
437
+ : [
438
+ `Seeded ralplan ${summary.mode} run (${resolved.interactive ? "interactive" : "automated"}) at ${statePath}.`,
439
+ `Active run_id: ${runId}`,
440
+ resolved.architectKind ? `Architect: ${resolved.architectKind}` : undefined,
441
+ resolved.criticKind ? `Critic: ${resolved.criticKind}` : undefined,
442
+ "Run `/skill:ralplan` inside the GJC agent to execute the consensus loop.",
443
+ "",
444
+ ]
445
+ .filter((line): line is string => Boolean(line))
446
+ .join("\n");
447
+ return { status: 0, stdout };
448
+ }
449
+
450
+ /* -------------------------------- entry --------------------------------- */
451
+
452
+ export async function runNativeRalplanCommand(args: string[], cwd = process.cwd()): Promise<RalplanCommandResult> {
453
+ try {
454
+ if (isRalplanArtifactWriteInvocation(args)) return await handleArtifactWrite(args, cwd);
455
+ return await handleConsensusHandoff(args, cwd);
456
+ } catch (error) {
457
+ if (error instanceof RalplanCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
458
+ return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
459
+ }
460
+ }