@gajae-code/coding-agent 0.2.1 → 0.2.2

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 (101) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/types/commands/contribution-prep.d.ts +18 -0
  3. package/dist/types/commands/session.d.ts +24 -0
  4. package/dist/types/config/model-registry.d.ts +2 -2
  5. package/dist/types/config/models-config-schema.d.ts +17 -9
  6. package/dist/types/config/settings-schema.d.ts +1 -24
  7. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  8. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  9. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  12. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  15. package/dist/types/goals/runtime.d.ts +3 -9
  16. package/dist/types/goals/state.d.ts +3 -6
  17. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  18. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  19. package/dist/types/modes/components/status-line.d.ts +0 -3
  20. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  21. package/dist/types/modes/interactive-mode.d.ts +1 -12
  22. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  23. package/dist/types/modes/theme/theme.d.ts +1 -2
  24. package/dist/types/modes/types.d.ts +1 -7
  25. package/dist/types/session/agent-session.d.ts +2 -0
  26. package/dist/types/session/contribution-prep.d.ts +47 -0
  27. package/dist/types/skill-state/active-state.d.ts +4 -0
  28. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  29. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  30. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  31. package/package.json +7 -7
  32. package/src/cli/args.ts +3 -2
  33. package/src/cli.ts +6 -1
  34. package/src/commands/contribution-prep.ts +41 -0
  35. package/src/commands/deep-interview.ts +6 -22
  36. package/src/commands/launch.ts +10 -1
  37. package/src/commands/ralplan.ts +10 -22
  38. package/src/commands/session.ts +150 -0
  39. package/src/commands/state.ts +14 -4
  40. package/src/commands/team.ts +23 -3
  41. package/src/config/model-registry.ts +10 -2
  42. package/src/config/models-config-schema.ts +120 -102
  43. package/src/config/settings-schema.ts +1 -25
  44. package/src/config.ts +1 -1
  45. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  47. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  48. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  49. package/src/eval/py/prelude.py +1 -1
  50. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  51. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  52. package/src/gjc-runtime/launch-tmux.ts +83 -43
  53. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  54. package/src/gjc-runtime/state-runtime.ts +562 -0
  55. package/src/gjc-runtime/team-runtime.ts +708 -52
  56. package/src/gjc-runtime/tmux-common.ts +119 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  58. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  59. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  60. package/src/goals/runtime.ts +38 -144
  61. package/src/goals/state.ts +36 -7
  62. package/src/goals/tools/goal-tool.ts +15 -172
  63. package/src/hooks/skill-state.ts +31 -12
  64. package/src/internal-urls/docs-index.generated.ts +4 -3
  65. package/src/modes/components/skill-hud/render.ts +4 -0
  66. package/src/modes/components/status-line/segments.ts +5 -16
  67. package/src/modes/components/status-line/types.ts +0 -3
  68. package/src/modes/components/status-line.ts +0 -6
  69. package/src/modes/controllers/command-controller.ts +25 -1
  70. package/src/modes/controllers/input-controller.ts +0 -15
  71. package/src/modes/interactive-mode.ts +18 -219
  72. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  73. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  74. package/src/modes/theme/theme.ts +0 -6
  75. package/src/modes/types.ts +1 -7
  76. package/src/prompts/goals/goal-continuation.md +1 -4
  77. package/src/prompts/goals/goal-mode-active.md +3 -5
  78. package/src/prompts/system/system-prompt.md +5 -7
  79. package/src/prompts/tools/goal.md +4 -4
  80. package/src/sdk.ts +1 -1
  81. package/src/session/agent-session.ts +18 -0
  82. package/src/session/contribution-prep.ts +320 -0
  83. package/src/skill-state/active-state.ts +38 -0
  84. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  85. package/src/skill-state/workflow-hud.ts +23 -5
  86. package/src/skill-state/workflow-state-contract.ts +121 -0
  87. package/src/slash-commands/builtin-registry.ts +24 -12
  88. package/src/task/commands.ts +1 -5
  89. package/src/tools/gh.ts +212 -2
  90. package/src/tools/index.ts +2 -5
  91. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  92. package/dist/types/commands/question.d.ts +0 -7
  93. package/dist/types/modes/loop-limit.d.ts +0 -22
  94. package/src/commands/gjc-runtime-bridge.ts +0 -227
  95. package/src/commands/question.ts +0 -12
  96. package/src/modes/loop-limit.ts +0 -140
  97. package/src/prompts/commands/orchestrate.md +0 -49
  98. package/src/prompts/goals/goal-budget-limit.md +0 -16
  99. package/src/prompts/tools/create-goal.md +0 -3
  100. package/src/prompts/tools/get-goal.md +0 -3
  101. package/src/prompts/tools/update-goal.md +0 -3
@@ -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
+ }