@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
@@ -0,0 +1,546 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { syncSkillActiveState } from "../skill-state/active-state";
6
+ import { buildDeepInterviewHudSummary } from "../skill-state/workflow-hud";
7
+ import { runNativeRalplanCommand } from "./ralplan-runtime";
8
+ import { runNativeStateCommand } from "./state-runtime";
9
+
10
+ /**
11
+ * Native implementation of `gjc deep-interview`.
12
+ *
13
+ * The CLI itself does not run the Socratic interview; that lives inside the `/skill:deep-interview`
14
+ * skill executed by the agent. This handler validates the documented argument-hint surface
15
+ * (`[--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
16
+ * updates the shared HUD rail via `syncSkillActiveState` so the active interview is visible to
17
+ * the TUI.
18
+ */
19
+
20
+ export interface DeepInterviewCommandResult {
21
+ status: number;
22
+ stdout?: string;
23
+ stderr?: string;
24
+ }
25
+
26
+ const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
27
+
28
+ const DEFAULT_AMBIGUITY_THRESHOLD = 0.05;
29
+
30
+ const RESOLUTION_THRESHOLDS = {
31
+ quick: 0.6,
32
+ standard: 0.5,
33
+ deep: 0.35,
34
+ } as const;
35
+
36
+ type DeepInterviewResolution = keyof typeof RESOLUTION_THRESHOLDS;
37
+
38
+ class DeepInterviewCommandError extends Error {
39
+ constructor(
40
+ public readonly exitStatus: number,
41
+ message: string,
42
+ ) {
43
+ super(message);
44
+ this.name = "DeepInterviewCommandError";
45
+ }
46
+ }
47
+
48
+ const VALUE_FLAGS = new Set([
49
+ "--session-id",
50
+ "--threshold",
51
+ "--threshold-source",
52
+ "--stage",
53
+ "--slug",
54
+ "--spec",
55
+ "--handoff",
56
+ ]);
57
+
58
+ function flagValue(args: readonly string[], flag: string): string | undefined {
59
+ const index = args.indexOf(flag);
60
+ if (index < 0) return undefined;
61
+ return args[index + 1];
62
+ }
63
+
64
+ function hasFlag(args: readonly string[], flag: string): boolean {
65
+ return args.includes(flag);
66
+ }
67
+
68
+ function assertSafePathComponent(value: string, label: string): void {
69
+ if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
70
+ throw new DeepInterviewCommandError(2, `invalid path component for --${label}: ${value}`);
71
+ }
72
+ }
73
+
74
+ function encodeSessionSegment(value: string): string {
75
+ return encodeURIComponent(value).replaceAll(".", "%2E");
76
+ }
77
+
78
+ function defaultSpecSlug(now: Date = new Date()): string {
79
+ const yyyy = now.getUTCFullYear().toString().padStart(4, "0");
80
+ const mm = (now.getUTCMonth() + 1).toString().padStart(2, "0");
81
+ const dd = now.getUTCDate().toString().padStart(2, "0");
82
+ const hh = now.getUTCHours().toString().padStart(2, "0");
83
+ const min = now.getUTCMinutes().toString().padStart(2, "0");
84
+ return `${yyyy}-${mm}-${dd}-${hh}${min}-${randomBytes(2).toString("hex")}`;
85
+ }
86
+
87
+ function stateDirFor(cwd: string, sessionId: string | undefined): string {
88
+ return sessionId
89
+ ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(sessionId))
90
+ : path.join(cwd, ".gjc", "state");
91
+ }
92
+
93
+ function deepInterviewStatePath(cwd: string, sessionId: string | undefined): string {
94
+ return path.join(stateDirFor(cwd, sessionId), "deep-interview-state.json");
95
+ }
96
+
97
+ async function readJsonObject(filePath: string): Promise<Record<string, unknown>> {
98
+ try {
99
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf-8"));
100
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
101
+ } catch {
102
+ // Missing/corrupt state should not prevent the sanctioned persistence CLI from writing a receipt.
103
+ }
104
+ return {};
105
+ }
106
+
107
+ async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {
108
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
109
+ const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
110
+ await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
111
+ await fs.rename(tmp, filePath);
112
+ }
113
+
114
+ async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string> {
115
+ const candidate = path.isAbsolute(rawSpec) ? rawSpec : path.resolve(cwd, rawSpec);
116
+ try {
117
+ const stat = await fs.stat(candidate);
118
+ if (stat.isFile()) return await fs.readFile(candidate, "utf-8");
119
+ } catch (error) {
120
+ const err = error as NodeJS.ErrnoException;
121
+ if (err.code !== "ENOENT" && err.code !== "ENOTDIR") {
122
+ throw new DeepInterviewCommandError(2, `failed to read --spec ${candidate}: ${err.message}`);
123
+ }
124
+ }
125
+ return rawSpec;
126
+ }
127
+
128
+ interface ResolvedDeepInterviewArgs {
129
+ resolution: DeepInterviewResolution;
130
+ threshold: number;
131
+ thresholdSource: string;
132
+ sessionId?: string;
133
+ idea: string;
134
+ json: boolean;
135
+ }
136
+
137
+ export interface ResolvedDeepInterviewSpecWriteArgs {
138
+ stage: "final";
139
+ slug: string;
140
+ spec: string;
141
+ sessionId?: string;
142
+ json: boolean;
143
+ deliberate: boolean;
144
+ handoff?: "ralplan";
145
+ }
146
+
147
+ export interface PersistedDeepInterviewSpec {
148
+ slug: string;
149
+ path: string;
150
+ stage: "final";
151
+ sha256: string;
152
+ createdAt: string;
153
+ statePath: string;
154
+ }
155
+
156
+ interface DeepInterviewSpecWriteSummary {
157
+ skill: "deep-interview";
158
+ stage: "final";
159
+ slug: string;
160
+ path: string;
161
+ sha256: string;
162
+ created_at: string;
163
+ state_path: string;
164
+ handoff?: {
165
+ to: "ralplan";
166
+ mode: "deliberate";
167
+ state_path?: string;
168
+ run_id?: string;
169
+ };
170
+ }
171
+
172
+ async function readSettingsAmbiguityThreshold(
173
+ settingsPath: string,
174
+ ): Promise<{ threshold: number; source: string } | undefined> {
175
+ let raw: string;
176
+ try {
177
+ raw = await fs.readFile(settingsPath, "utf-8");
178
+ } catch (error) {
179
+ const err = error as NodeJS.ErrnoException;
180
+ if (err.code === "ENOENT") return undefined;
181
+ return undefined;
182
+ }
183
+ let parsed: unknown;
184
+ try {
185
+ parsed = JSON.parse(raw);
186
+ } catch {
187
+ return undefined;
188
+ }
189
+ const candidate = (parsed as { gjc?: { deepInterview?: { ambiguityThreshold?: unknown } } })?.gjc?.deepInterview
190
+ ?.ambiguityThreshold;
191
+ if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate <= 0 || candidate > 1) {
192
+ return undefined;
193
+ }
194
+ return { threshold: candidate, source: settingsPath };
195
+ }
196
+
197
+ async function resolveConfiguredAmbiguityThreshold(
198
+ cwd: string,
199
+ ): Promise<{ threshold: number; source: string } | undefined> {
200
+ const projectSettings = path.join(cwd, ".gjc", "settings.json");
201
+ const projectValue = await readSettingsAmbiguityThreshold(projectSettings);
202
+ if (projectValue) return projectValue;
203
+ const configDir = process.env.GJC_CONFIG_DIR?.trim() || path.join(os.homedir(), ".gjc");
204
+ const userSettings = path.join(configDir, "settings.json");
205
+ return await readSettingsAmbiguityThreshold(userSettings);
206
+ }
207
+
208
+ function isDeepInterviewSpecWriteInvocation(args: readonly string[]): boolean {
209
+ return hasFlag(args, "--write");
210
+ }
211
+
212
+ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewSpecWriteArgs> {
213
+ const stage = flagValue(args, "--stage")?.trim() || "final";
214
+ if (stage !== "final") {
215
+ throw new DeepInterviewCommandError(2, 'unknown --stage for deep-interview --write: expected "final"');
216
+ }
217
+
218
+ const slug = flagValue(args, "--slug")?.trim() || defaultSpecSlug();
219
+ assertSafePathComponent(slug, "slug");
220
+
221
+ const rawSpec = flagValue(args, "--spec");
222
+ if (rawSpec === undefined || rawSpec === "") {
223
+ throw new DeepInterviewCommandError(2, "--spec is required for deep-interview --write");
224
+ }
225
+
226
+ const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
227
+ if (sessionId) assertSafePathComponent(sessionId, "session-id");
228
+
229
+ const rawHandoff = flagValue(args, "--handoff")?.trim() || undefined;
230
+ if (rawHandoff && rawHandoff !== "ralplan") {
231
+ throw new DeepInterviewCommandError(2, 'unknown --handoff target: expected "ralplan"');
232
+ }
233
+
234
+ const allowedFlags = new Set([
235
+ "--write",
236
+ "--stage",
237
+ "--slug",
238
+ "--spec",
239
+ "--session-id",
240
+ "--handoff",
241
+ "--deliberate",
242
+ "--json",
243
+ ]);
244
+ let skipNext = false;
245
+ for (const arg of args) {
246
+ if (skipNext) {
247
+ skipNext = false;
248
+ continue;
249
+ }
250
+ if (["--stage", "--slug", "--spec", "--session-id", "--handoff"].includes(arg)) {
251
+ skipNext = true;
252
+ continue;
253
+ }
254
+ if (arg.startsWith("-") && !allowedFlags.has(arg)) {
255
+ throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview --write: ${arg}`);
256
+ }
257
+ }
258
+
259
+ return {
260
+ stage: "final",
261
+ slug,
262
+ spec: await resolveSpecContent(rawSpec, cwd),
263
+ sessionId,
264
+ json: hasFlag(args, "--json"),
265
+ deliberate: hasFlag(args, "--deliberate"),
266
+ handoff: rawHandoff as "ralplan" | undefined,
267
+ };
268
+ }
269
+
270
+ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
271
+ const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
272
+ if (sessionId) assertSafePathComponent(sessionId, "session-id");
273
+
274
+ const explicitResolutions = (["quick", "standard", "deep"] as const).filter(name => hasFlag(args, `--${name}`));
275
+ if (explicitResolutions.length > 1) {
276
+ throw new DeepInterviewCommandError(2, "pass at most one of --quick, --standard, --deep");
277
+ }
278
+ const resolution: DeepInterviewResolution | undefined = explicitResolutions[0];
279
+
280
+ // Precedence: --threshold > settings.json (project then user) > resolution flag default > 0.05.
281
+ let threshold: number = DEFAULT_AMBIGUITY_THRESHOLD;
282
+ let thresholdSource = "default";
283
+ const thresholdOverride = flagValue(args, "--threshold");
284
+ if (thresholdOverride !== undefined) {
285
+ const parsed = Number(thresholdOverride);
286
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
287
+ throw new DeepInterviewCommandError(
288
+ 2,
289
+ `invalid --threshold: ${thresholdOverride}. Expected 0 < threshold <= 1.`,
290
+ );
291
+ }
292
+ threshold = parsed;
293
+ thresholdSource = flagValue(args, "--threshold-source")?.trim() || "flag:--threshold";
294
+ } else {
295
+ const configured = await resolveConfiguredAmbiguityThreshold(cwd);
296
+ if (configured) {
297
+ threshold = configured.threshold;
298
+ thresholdSource = configured.source;
299
+ } else if (resolution) {
300
+ threshold = RESOLUTION_THRESHOLDS[resolution];
301
+ thresholdSource = `flag:--${resolution}`;
302
+ }
303
+ }
304
+
305
+ const ideaParts: string[] = [];
306
+ let skipNext = false;
307
+ for (const arg of args) {
308
+ if (skipNext) {
309
+ skipNext = false;
310
+ continue;
311
+ }
312
+ if (VALUE_FLAGS.has(arg)) {
313
+ skipNext = true;
314
+ continue;
315
+ }
316
+ if (arg === "--quick" || arg === "--standard" || arg === "--deep" || arg === "--json") continue;
317
+ if (arg.startsWith("-")) {
318
+ throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview: ${arg}`);
319
+ }
320
+ ideaParts.push(arg);
321
+ }
322
+ const idea = ideaParts.join(" ").trim();
323
+ const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
324
+ return {
325
+ resolution: effectiveResolution,
326
+ threshold,
327
+ thresholdSource,
328
+ sessionId,
329
+ idea,
330
+ json: hasFlag(args, "--json"),
331
+ };
332
+ }
333
+
334
+ export async function persistDeepInterviewSpec(
335
+ cwd: string,
336
+ resolved: ResolvedDeepInterviewSpecWriteArgs,
337
+ ): Promise<PersistedDeepInterviewSpec> {
338
+ const specsDir = path.join(cwd, ".gjc", "specs");
339
+ await fs.mkdir(specsDir, { recursive: true });
340
+ const specPath = path.join(specsDir, `deep-interview-${resolved.slug}.md`);
341
+ const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
342
+ await fs.writeFile(specPath, content);
343
+
344
+ const sha256 = createHash("sha256").update(content).digest("hex");
345
+ const createdAt = new Date().toISOString();
346
+ await fs.appendFile(
347
+ path.join(specsDir, "deep-interview-index.jsonl"),
348
+ `${JSON.stringify({ slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 })}\n`,
349
+ );
350
+
351
+ const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
352
+ const existing = await readJsonObject(statePath);
353
+ const payload: Record<string, unknown> = {
354
+ ...existing,
355
+ active: true,
356
+ current_phase: "handoff",
357
+ skill: "deep-interview",
358
+ version: typeof existing.version === "number" ? existing.version : 1,
359
+ spec_slug: resolved.slug,
360
+ spec_path: specPath,
361
+ spec_sha256: sha256,
362
+ spec_stage: resolved.stage,
363
+ spec_persisted_at: createdAt,
364
+ updated_at: createdAt,
365
+ };
366
+ if (resolved.sessionId) payload.session_id = resolved.sessionId;
367
+ await writeJsonAtomic(statePath, payload);
368
+ await syncDeepInterviewHud({
369
+ cwd,
370
+ sessionId: resolved.sessionId,
371
+ phase: "handoff",
372
+ specStatus: "persisted",
373
+ });
374
+
375
+ return {
376
+ slug: resolved.slug,
377
+ path: specPath,
378
+ stage: resolved.stage,
379
+ sha256,
380
+ createdAt,
381
+ statePath,
382
+ };
383
+ }
384
+
385
+ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
386
+ const stateDir = resolved.sessionId
387
+ ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
388
+ : path.join(cwd, ".gjc", "state");
389
+ await fs.mkdir(stateDir, { recursive: true });
390
+ const statePath = path.join(stateDir, "deep-interview-state.json");
391
+ const now = new Date().toISOString();
392
+ const payload: Record<string, unknown> = {
393
+ active: true,
394
+ current_phase: "interviewing",
395
+ skill: "deep-interview",
396
+ resolution: resolved.resolution,
397
+ threshold: resolved.threshold,
398
+ threshold_source: resolved.thresholdSource,
399
+ state: {
400
+ initial_idea: resolved.idea,
401
+ rounds: [],
402
+ current_ambiguity: 1.0,
403
+ threshold: resolved.threshold,
404
+ threshold_source: resolved.thresholdSource,
405
+ },
406
+ updated_at: now,
407
+ };
408
+ if (resolved.sessionId) payload.session_id = resolved.sessionId;
409
+ await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
410
+ return statePath;
411
+ }
412
+
413
+ async function syncDeepInterviewHud(options: {
414
+ cwd: string;
415
+ sessionId?: string;
416
+ phase: string;
417
+ ambiguity?: number;
418
+ threshold?: number;
419
+ roundCount?: number;
420
+ specStatus?: string;
421
+ }): Promise<void> {
422
+ try {
423
+ await syncSkillActiveState({
424
+ cwd: options.cwd,
425
+ skill: "deep-interview",
426
+ active: options.phase !== "complete",
427
+ phase: options.phase,
428
+ sessionId: options.sessionId,
429
+ source: "gjc-deep-interview-native",
430
+ hud: buildDeepInterviewHudSummary({
431
+ phase: options.phase,
432
+ ambiguity: options.ambiguity,
433
+ threshold: options.threshold,
434
+ roundCount: options.roundCount,
435
+ specStatus: options.specStatus,
436
+ updatedAt: new Date().toISOString(),
437
+ }),
438
+ });
439
+ } catch {
440
+ // HUD sync is best-effort and must not change command semantics.
441
+ }
442
+ }
443
+
444
+ async function handleSpecWrite(args: readonly string[], cwd: string): Promise<DeepInterviewCommandResult> {
445
+ const resolved = await resolveSpecWriteArgs(args, cwd);
446
+ const persisted = await persistDeepInterviewSpec(cwd, resolved);
447
+ const shouldHandoff = resolved.deliberate || resolved.handoff === "ralplan";
448
+ const summary: DeepInterviewSpecWriteSummary = {
449
+ skill: "deep-interview",
450
+ stage: persisted.stage,
451
+ slug: persisted.slug,
452
+ path: persisted.path,
453
+ sha256: persisted.sha256,
454
+ created_at: persisted.createdAt,
455
+ state_path: persisted.statePath,
456
+ };
457
+
458
+ if (shouldHandoff) {
459
+ const ralplanArgs = ["--deliberate", "--json"];
460
+ if (resolved.sessionId) ralplanArgs.push("--session-id", resolved.sessionId);
461
+ ralplanArgs.push(persisted.path);
462
+ const ralplanResult = await runNativeRalplanCommand(ralplanArgs, cwd);
463
+ if (ralplanResult.status !== 0) {
464
+ throw new DeepInterviewCommandError(
465
+ ralplanResult.status,
466
+ ralplanResult.stderr?.trim() || "failed to seed ralplan",
467
+ );
468
+ }
469
+
470
+ const handoffArgs = ["handoff", "--mode", "deep-interview", "--to", "ralplan", "--json"];
471
+ if (resolved.sessionId) handoffArgs.push("--session-id", resolved.sessionId);
472
+ const handoffResult = await runNativeStateCommand(handoffArgs, cwd);
473
+ if (handoffResult.status !== 0) {
474
+ throw new DeepInterviewCommandError(
475
+ handoffResult.status,
476
+ handoffResult.stderr?.trim() || "failed to hand off deep-interview to ralplan",
477
+ );
478
+ }
479
+
480
+ const ralplanPayload = ralplanResult.stdout ? (JSON.parse(ralplanResult.stdout) as Record<string, unknown>) : {};
481
+ summary.handoff = {
482
+ to: "ralplan",
483
+ mode: "deliberate",
484
+ state_path: typeof ralplanPayload.state_path === "string" ? ralplanPayload.state_path : undefined,
485
+ run_id: typeof ralplanPayload.run_id === "string" ? ralplanPayload.run_id : undefined,
486
+ };
487
+ }
488
+
489
+ const stdout = resolved.json
490
+ ? `${JSON.stringify(summary, null, 2)}\n`
491
+ : [
492
+ `Persisted deep-interview ${persisted.stage} spec at ${persisted.path}.`,
493
+ shouldHandoff ? "Handed off deep-interview to ralplan (deliberate)." : undefined,
494
+ "",
495
+ ]
496
+ .filter((line): line is string => Boolean(line))
497
+ .join("\n");
498
+ return { status: 0, stdout };
499
+ }
500
+
501
+ export async function runNativeDeepInterviewCommand(
502
+ args: string[],
503
+ cwd = process.cwd(),
504
+ ): Promise<DeepInterviewCommandResult> {
505
+ try {
506
+ if (isDeepInterviewSpecWriteInvocation(args)) return await handleSpecWrite(args, cwd);
507
+ const resolved = await resolveDeepInterviewArgs(args, cwd);
508
+ if (!resolved.idea) {
509
+ throw new DeepInterviewCommandError(
510
+ 2,
511
+ 'gjc deep-interview requires an idea, e.g. `gjc deep-interview "<idea>"`.',
512
+ );
513
+ }
514
+ const statePath = await seedDeepInterviewState(cwd, resolved);
515
+ await syncDeepInterviewHud({
516
+ cwd,
517
+ sessionId: resolved.sessionId,
518
+ phase: "interviewing",
519
+ ambiguity: 1,
520
+ threshold: resolved.threshold,
521
+ roundCount: 0,
522
+ });
523
+
524
+ const summary = {
525
+ skill: "deep-interview",
526
+ resolution: resolved.resolution,
527
+ threshold: resolved.threshold,
528
+ threshold_source: resolved.thresholdSource,
529
+ idea: resolved.idea,
530
+ state_path: statePath,
531
+ handoff: "Run `/skill:deep-interview` inside the GJC agent to drive the Socratic interview loop.",
532
+ };
533
+ const stdout = resolved.json
534
+ ? `${JSON.stringify(summary, null, 2)}\n`
535
+ : [
536
+ `Seeded deep-interview ${resolved.resolution} run at ${statePath}.`,
537
+ `Threshold: ${(resolved.threshold * 100).toFixed(0)}% (source: ${resolved.thresholdSource}).`,
538
+ "Run `/skill:deep-interview` inside the GJC agent to execute the interview.",
539
+ "",
540
+ ].join("\n");
541
+ return { status: 0, stdout };
542
+ } catch (error) {
543
+ if (error instanceof DeepInterviewCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
544
+ return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
545
+ }
546
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { Snowflake } from "@gajae-code/utils";
4
- import type { Goal, GoalModeState } from "../goals/state";
4
+ import { type Goal, type GoalModeState, normalizeGoal } from "../goals/state";
5
5
  import {
6
6
  buildSessionContext,
7
7
  loadEntriesFromFile,
@@ -94,24 +94,7 @@ export async function writePendingGoalModeRequest(input: {
94
94
  }
95
95
 
96
96
  function goalFromModeData(modeData: Record<string, unknown> | undefined): Goal | null {
97
- const candidate = modeData?.goal;
98
- if (typeof candidate !== "object" || candidate === null) return null;
99
- const goal = candidate as Partial<Goal>;
100
- if (
101
- typeof goal.id !== "string" ||
102
- typeof goal.objective !== "string" ||
103
- typeof goal.status !== "string" ||
104
- typeof goal.tokensUsed !== "number" ||
105
- typeof goal.timeUsedSeconds !== "number" ||
106
- typeof goal.createdAt !== "number" ||
107
- typeof goal.updatedAt !== "number"
108
- ) {
109
- return null;
110
- }
111
- if (!["active", "paused", "budget-limited", "complete", "dropped"].includes(goal.status)) {
112
- return null;
113
- }
114
- return goal as Goal;
97
+ return normalizeGoal(modeData?.goal);
115
98
  }
116
99
 
117
100
  function isNonTerminalGoal(goal: Goal | null): goal is Goal {