@gajae-code/coding-agent 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -8600
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +41 -0
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +56 -0
- package/dist/types/defaults/gjc-defaults.d.ts +19 -6
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +18 -0
- package/dist/types/hooks/skill-state.d.ts +5 -0
- package/dist/types/memories/index.d.ts +1 -1
- package/dist/types/memory-backend/local-backend.d.ts +3 -3
- package/dist/types/modes/components/hook-selector.d.ts +7 -0
- package/dist/types/modes/components/settings-selector.d.ts +3 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +126 -0
- package/dist/types/modes/theme/theme.d.ts +5 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -2
- package/dist/types/sdk.d.ts +6 -2
- package/dist/types/session/agent-session.d.ts +45 -1
- package/dist/types/session/session-manager.d.ts +3 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
- package/dist/types/setup/provider-onboarding.d.ts +29 -5
- package/dist/types/skill-state/active-state.d.ts +26 -1
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/initial-phase.d.ts +12 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +11 -0
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/skill.d.ts +47 -0
- package/dist/types/utils/changelog.d.ts +18 -2
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +26 -12
- package/src/cli/update-cli.ts +67 -16
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +25 -2
- package/src/commands/setup.ts +2 -0
- package/src/commands/state.ts +1 -0
- package/src/config/settings-schema.ts +63 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +58 -5
- package/src/defaults/gjc/skills/deep-interview/auto-answer-uncertain.md +37 -0
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +42 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -0
- package/src/defaults/gjc/skills/team/SKILL.md +10 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +19 -6
- package/src/defaults/gjc-defaults.ts +68 -16
- package/src/discovery/helpers.ts +24 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +312 -1
- package/src/gjc-runtime/state-runtime.ts +175 -5
- package/src/goals/tools/goal-tool.ts +5 -1
- package/src/hooks/skill-state.ts +8 -6
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/internal-urls/memory-protocol.ts +3 -2
- package/src/main.ts +2 -3
- package/src/memories/index.ts +6 -4
- package/src/memory-backend/local-backend.ts +14 -6
- package/src/modes/components/hook-selector.ts +156 -1
- package/src/modes/components/settings-selector.ts +16 -12
- package/src/modes/controllers/command-controller.ts +3 -4
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +69 -9
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/theme/defaults/blue-crab.json +126 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/theme.ts +40 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/context-usage.ts +66 -17
- package/src/prompts/agents/architect.md +3 -0
- package/src/prompts/agents/executor.md +2 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/memories/unavailable.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +6 -0
- package/src/prompts/tools/skill.md +28 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/sdk.ts +54 -10
- package/src/session/agent-session.ts +204 -21
- package/src/session/session-manager.ts +9 -1
- package/src/setup/model-onboarding-guidance.ts +6 -3
- package/src/setup/provider-onboarding.ts +177 -16
- package/src/skill-state/active-state.ts +150 -25
- package/src/skill-state/deep-interview-mutation-guard.ts +11 -24
- package/src/skill-state/initial-phase.ts +17 -0
- package/src/slash-commands/builtin-registry.ts +62 -14
- package/src/slash-commands/helpers/context-report.ts +123 -13
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +91 -4
- package/src/task/types.ts +6 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/index.ts +23 -1
- package/src/tools/skill.ts +153 -0
- package/src/utils/changelog.ts +67 -44
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { getAgentDir, isEnoent, parseFrontmatter } from "@gajae-code/utils";
|
|
3
|
+
import autoAnswerUncertainFragment from "./gjc/skills/deep-interview/auto-answer-uncertain.md" with { type: "text" };
|
|
4
|
+
import autoResearchGreenfieldFragment from "./gjc/skills/deep-interview/auto-research-greenfield.md" with {
|
|
5
|
+
type: "text",
|
|
6
|
+
};
|
|
3
7
|
import deepInterviewSkill from "./gjc/skills/deep-interview/SKILL.md" with { type: "text" };
|
|
4
8
|
import ralplanSkill from "./gjc/skills/ralplan/SKILL.md" with { type: "text" };
|
|
5
9
|
import teamSkill from "./gjc/skills/team/SKILL.md" with { type: "text" };
|
|
@@ -7,7 +11,7 @@ import ultragoalSkill from "./gjc/skills/ultragoal/SKILL.md" with { type: "text"
|
|
|
7
11
|
|
|
8
12
|
export const DEFAULT_GJC_DEFINITION_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
|
|
9
13
|
export type DefaultGjcDefinitionName = (typeof DEFAULT_GJC_DEFINITION_NAMES)[number];
|
|
10
|
-
export type DefaultGjcDefinitionKind = "skill";
|
|
14
|
+
export type DefaultGjcDefinitionKind = "skill" | "skill-fragment";
|
|
11
15
|
export type EmbeddedDefaultGjcSkill = {
|
|
12
16
|
name: DefaultGjcDefinitionName;
|
|
13
17
|
description: string;
|
|
@@ -19,25 +23,41 @@ export type EmbeddedDefaultGjcSkill = {
|
|
|
19
23
|
};
|
|
20
24
|
export type DefaultGjcInstallStatus = "different" | "matching" | "missing" | "skipped" | "written";
|
|
21
25
|
|
|
22
|
-
export interface
|
|
23
|
-
kind:
|
|
26
|
+
export interface DefaultGjcSkillDefinition {
|
|
27
|
+
kind: "skill";
|
|
24
28
|
name: DefaultGjcDefinitionName;
|
|
25
29
|
relativePath: string;
|
|
26
30
|
content: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
export interface DefaultGjcSkillFragmentDefinition {
|
|
34
|
+
kind: "skill-fragment";
|
|
35
|
+
parentSkillName: DefaultGjcDefinitionName;
|
|
36
|
+
relativePath: string;
|
|
37
|
+
content: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DefaultGjcDefinition = DefaultGjcSkillDefinition | DefaultGjcSkillFragmentDefinition;
|
|
41
|
+
|
|
29
42
|
export interface InstallDefaultGjcDefinitionsOptions {
|
|
30
43
|
check?: boolean;
|
|
31
44
|
force?: boolean;
|
|
32
45
|
targetRoot?: string;
|
|
33
46
|
}
|
|
34
47
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
export type DefaultGjcDefinitionInstallFile =
|
|
49
|
+
| {
|
|
50
|
+
kind: "skill";
|
|
51
|
+
name: DefaultGjcDefinitionName;
|
|
52
|
+
path: string;
|
|
53
|
+
status: DefaultGjcInstallStatus;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
kind: "skill-fragment";
|
|
57
|
+
parentSkillName: DefaultGjcDefinitionName;
|
|
58
|
+
path: string;
|
|
59
|
+
status: DefaultGjcInstallStatus;
|
|
60
|
+
};
|
|
41
61
|
|
|
42
62
|
export interface DefaultGjcDefinitionInstallResult {
|
|
43
63
|
targetRoot: string;
|
|
@@ -60,6 +80,18 @@ const DEFAULT_GJC_DEFINITIONS: readonly DefaultGjcDefinition[] = [
|
|
|
60
80
|
{ kind: "skill", name: "ralplan", relativePath: "skills/ralplan/SKILL.md", content: ralplanSkill },
|
|
61
81
|
{ kind: "skill", name: "team", relativePath: "skills/team/SKILL.md", content: teamSkill },
|
|
62
82
|
{ kind: "skill", name: "ultragoal", relativePath: "skills/ultragoal/SKILL.md", content: ultragoalSkill },
|
|
83
|
+
{
|
|
84
|
+
kind: "skill-fragment",
|
|
85
|
+
parentSkillName: "deep-interview",
|
|
86
|
+
relativePath: "skill-fragments/deep-interview/auto-research-greenfield.md",
|
|
87
|
+
content: autoResearchGreenfieldFragment,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
kind: "skill-fragment",
|
|
91
|
+
parentSkillName: "deep-interview",
|
|
92
|
+
relativePath: "skill-fragments/deep-interview/auto-answer-uncertain.md",
|
|
93
|
+
content: autoAnswerUncertainFragment,
|
|
94
|
+
},
|
|
63
95
|
];
|
|
64
96
|
|
|
65
97
|
export function getDefaultGjcDefinitions(): readonly DefaultGjcDefinition[] {
|
|
@@ -70,8 +102,19 @@ export function getDefaultGjcAgentDefinitions(): readonly DefaultGjcDefinition[]
|
|
|
70
102
|
return [];
|
|
71
103
|
}
|
|
72
104
|
|
|
105
|
+
export function getEmbeddedDefaultGjcSkillFragments(
|
|
106
|
+
parentSkillName: DefaultGjcDefinitionName,
|
|
107
|
+
): DefaultGjcSkillFragmentDefinition[] {
|
|
108
|
+
return DEFAULT_GJC_DEFINITIONS.filter(
|
|
109
|
+
(definition): definition is DefaultGjcSkillFragmentDefinition =>
|
|
110
|
+
definition.kind === "skill-fragment" && definition.parentSkillName === parentSkillName,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
73
114
|
export function getEmbeddedDefaultGjcSkills(): EmbeddedDefaultGjcSkill[] {
|
|
74
|
-
return DEFAULT_GJC_DEFINITIONS.filter(
|
|
115
|
+
return DEFAULT_GJC_DEFINITIONS.filter(
|
|
116
|
+
(definition): definition is DefaultGjcSkillDefinition => definition.kind === "skill",
|
|
117
|
+
).map(definition => {
|
|
75
118
|
const { frontmatter } = parseFrontmatter(definition.content, {
|
|
76
119
|
source: `embedded:gjc/${definition.relativePath}`,
|
|
77
120
|
level: "warn",
|
|
@@ -110,12 +153,21 @@ export async function installDefaultGjcDefinitions(
|
|
|
110
153
|
status = "written";
|
|
111
154
|
}
|
|
112
155
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
if (definition.kind === "skill") {
|
|
157
|
+
files.push({
|
|
158
|
+
kind: definition.kind,
|
|
159
|
+
name: definition.name,
|
|
160
|
+
path: destination,
|
|
161
|
+
status,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
files.push({
|
|
165
|
+
kind: definition.kind,
|
|
166
|
+
parentSkillName: definition.parentSkillName,
|
|
167
|
+
path: destination,
|
|
168
|
+
status,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
119
171
|
}
|
|
120
172
|
|
|
121
173
|
return summarizeInstallResult(targetRoot, files);
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getConfigDirName,
|
|
9
9
|
getPluginsDir,
|
|
10
10
|
getProjectDir,
|
|
11
|
+
logger,
|
|
11
12
|
parseFrontmatter,
|
|
12
13
|
tryParseJson,
|
|
13
14
|
} from "@gajae-code/utils";
|
|
@@ -16,6 +17,7 @@ import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../ca
|
|
|
16
17
|
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
17
18
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
18
19
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
20
|
+
import type { ForkContextPolicy } from "../task/types";
|
|
19
21
|
import { parseThinkingLevel } from "../thinking";
|
|
20
22
|
|
|
21
23
|
import { buildPluginDirRoot } from "./plugin-dir-roots";
|
|
@@ -214,6 +216,7 @@ export interface ParsedAgentFields {
|
|
|
214
216
|
autoloadSkills?: string[];
|
|
215
217
|
blocking?: boolean;
|
|
216
218
|
hide?: boolean;
|
|
219
|
+
forkContext?: ForkContextPolicy;
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
/**
|
|
@@ -267,10 +270,30 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
|
|
267
270
|
const model = parseModelList(frontmatter.model);
|
|
268
271
|
const blocking = parseBoolean(frontmatter.blocking);
|
|
269
272
|
const hide = parseBoolean(frontmatter.hide);
|
|
273
|
+
const forkContext = parseForkContextPolicy(frontmatter.forkContext);
|
|
270
274
|
const autoloadSkills = parseArrayOrCSV(frontmatter.autoloadSkills)
|
|
271
275
|
?.map(s => s.trim())
|
|
272
276
|
.filter(Boolean);
|
|
273
|
-
return {
|
|
277
|
+
return {
|
|
278
|
+
name,
|
|
279
|
+
description,
|
|
280
|
+
tools,
|
|
281
|
+
spawns,
|
|
282
|
+
model,
|
|
283
|
+
output,
|
|
284
|
+
thinkingLevel,
|
|
285
|
+
blocking,
|
|
286
|
+
autoloadSkills,
|
|
287
|
+
hide,
|
|
288
|
+
forkContext,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseForkContextPolicy(value: unknown): ForkContextPolicy | undefined {
|
|
293
|
+
if (value === undefined) return undefined;
|
|
294
|
+
if (value === "forbidden" || value === "allowed") return value;
|
|
295
|
+
logger.warn("Invalid agent forkContext frontmatter; expected 'allowed' or 'forbidden', ignoring", { value });
|
|
296
|
+
return undefined;
|
|
274
297
|
}
|
|
275
298
|
|
|
276
299
|
async function globIf(
|
|
@@ -110,6 +110,12 @@ export interface ExtensionUIDialogOptions {
|
|
|
110
110
|
onExternalEditor?: () => void;
|
|
111
111
|
/** Optional footer hint text rendered by interactive selector */
|
|
112
112
|
helpText?: string;
|
|
113
|
+
/**
|
|
114
|
+
* For interactive TUI select dialogs, render the focused option across
|
|
115
|
+
* multiple rows instead of truncating it. This is a select-only rendering
|
|
116
|
+
* hint; non-TUI bridges (RPC, ACP) drop it and do not serialize it.
|
|
117
|
+
*/
|
|
118
|
+
wrapFocused?: boolean;
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
/** Raw terminal input listener for extensions. */
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
5
6
|
import { buildDeepInterviewHudSummary } from "../skill-state/workflow-hud";
|
|
7
|
+
import { runNativeRalplanCommand } from "./ralplan-runtime";
|
|
8
|
+
import { runNativeStateCommand } from "./state-runtime";
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Native implementation of `gjc deep-interview`.
|
|
@@ -42,7 +45,15 @@ class DeepInterviewCommandError extends Error {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
const VALUE_FLAGS = new Set([
|
|
48
|
+
const VALUE_FLAGS = new Set([
|
|
49
|
+
"--session-id",
|
|
50
|
+
"--threshold",
|
|
51
|
+
"--threshold-source",
|
|
52
|
+
"--stage",
|
|
53
|
+
"--slug",
|
|
54
|
+
"--spec",
|
|
55
|
+
"--handoff",
|
|
56
|
+
]);
|
|
46
57
|
|
|
47
58
|
function flagValue(args: readonly string[], flag: string): string | undefined {
|
|
48
59
|
const index = args.indexOf(flag);
|
|
@@ -64,15 +75,108 @@ function encodeSessionSegment(value: string): string {
|
|
|
64
75
|
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
65
76
|
}
|
|
66
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
|
+
|
|
67
128
|
interface ResolvedDeepInterviewArgs {
|
|
68
129
|
resolution: DeepInterviewResolution;
|
|
69
130
|
threshold: number;
|
|
70
131
|
thresholdSource: string;
|
|
71
132
|
sessionId?: string;
|
|
72
133
|
idea: string;
|
|
134
|
+
language?: DeepInterviewLanguagePreference;
|
|
73
135
|
json: boolean;
|
|
74
136
|
}
|
|
75
137
|
|
|
138
|
+
interface DeepInterviewLanguagePreference {
|
|
139
|
+
code: "en" | "ko";
|
|
140
|
+
label: "English" | "Korean";
|
|
141
|
+
source: "explicit-user-request" | "initial-idea";
|
|
142
|
+
instruction: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface ResolvedDeepInterviewSpecWriteArgs {
|
|
146
|
+
stage: "final";
|
|
147
|
+
slug: string;
|
|
148
|
+
spec: string;
|
|
149
|
+
sessionId?: string;
|
|
150
|
+
json: boolean;
|
|
151
|
+
deliberate: boolean;
|
|
152
|
+
handoff?: "ralplan";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface PersistedDeepInterviewSpec {
|
|
156
|
+
slug: string;
|
|
157
|
+
path: string;
|
|
158
|
+
stage: "final";
|
|
159
|
+
sha256: string;
|
|
160
|
+
createdAt: string;
|
|
161
|
+
statePath: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface DeepInterviewSpecWriteSummary {
|
|
165
|
+
skill: "deep-interview";
|
|
166
|
+
stage: "final";
|
|
167
|
+
slug: string;
|
|
168
|
+
path: string;
|
|
169
|
+
sha256: string;
|
|
170
|
+
created_at: string;
|
|
171
|
+
state_path: string;
|
|
172
|
+
handoff?: {
|
|
173
|
+
to: "ralplan";
|
|
174
|
+
mode: "deliberate";
|
|
175
|
+
state_path?: string;
|
|
176
|
+
run_id?: string;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
76
180
|
async function readSettingsAmbiguityThreshold(
|
|
77
181
|
settingsPath: string,
|
|
78
182
|
): Promise<{ threshold: number; source: string } | undefined> {
|
|
@@ -109,6 +213,97 @@ async function resolveConfiguredAmbiguityThreshold(
|
|
|
109
213
|
return await readSettingsAmbiguityThreshold(userSettings);
|
|
110
214
|
}
|
|
111
215
|
|
|
216
|
+
function englishLanguagePreference(): DeepInterviewLanguagePreference {
|
|
217
|
+
return {
|
|
218
|
+
code: "en",
|
|
219
|
+
label: "English",
|
|
220
|
+
source: "explicit-user-request",
|
|
221
|
+
instruction:
|
|
222
|
+
"Ask every user-facing deep-interview question in English because the user explicitly requested English.",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveDeepInterviewLanguagePreference(idea: string): DeepInterviewLanguagePreference | undefined {
|
|
227
|
+
if (/\b(?:answer|ask|respond|reply|write|use|speak)\s+(?:only\s+)?in\s+English\b/i.test(idea)) {
|
|
228
|
+
return englishLanguagePreference();
|
|
229
|
+
}
|
|
230
|
+
if (/(?:영어로|영문으로|영어\s*(?:질문|답변|응답)|English\s+only)/i.test(idea)) {
|
|
231
|
+
return englishLanguagePreference();
|
|
232
|
+
}
|
|
233
|
+
if (/\p{Script=Hangul}/u.test(idea)) {
|
|
234
|
+
return {
|
|
235
|
+
code: "ko",
|
|
236
|
+
label: "Korean",
|
|
237
|
+
source: "initial-idea",
|
|
238
|
+
instruction:
|
|
239
|
+
"Ask every user-facing deep-interview question in Korean unless the user explicitly requests another language.",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isDeepInterviewSpecWriteInvocation(args: readonly string[]): boolean {
|
|
246
|
+
return hasFlag(args, "--write");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewSpecWriteArgs> {
|
|
250
|
+
const stage = flagValue(args, "--stage")?.trim() || "final";
|
|
251
|
+
if (stage !== "final") {
|
|
252
|
+
throw new DeepInterviewCommandError(2, 'unknown --stage for deep-interview --write: expected "final"');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const slug = flagValue(args, "--slug")?.trim() || defaultSpecSlug();
|
|
256
|
+
assertSafePathComponent(slug, "slug");
|
|
257
|
+
|
|
258
|
+
const rawSpec = flagValue(args, "--spec");
|
|
259
|
+
if (rawSpec === undefined || rawSpec === "") {
|
|
260
|
+
throw new DeepInterviewCommandError(2, "--spec is required for deep-interview --write");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
|
|
264
|
+
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
265
|
+
|
|
266
|
+
const rawHandoff = flagValue(args, "--handoff")?.trim() || undefined;
|
|
267
|
+
if (rawHandoff && rawHandoff !== "ralplan") {
|
|
268
|
+
throw new DeepInterviewCommandError(2, 'unknown --handoff target: expected "ralplan"');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const allowedFlags = new Set([
|
|
272
|
+
"--write",
|
|
273
|
+
"--stage",
|
|
274
|
+
"--slug",
|
|
275
|
+
"--spec",
|
|
276
|
+
"--session-id",
|
|
277
|
+
"--handoff",
|
|
278
|
+
"--deliberate",
|
|
279
|
+
"--json",
|
|
280
|
+
]);
|
|
281
|
+
let skipNext = false;
|
|
282
|
+
for (const arg of args) {
|
|
283
|
+
if (skipNext) {
|
|
284
|
+
skipNext = false;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (["--stage", "--slug", "--spec", "--session-id", "--handoff"].includes(arg)) {
|
|
288
|
+
skipNext = true;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (arg.startsWith("-") && !allowedFlags.has(arg)) {
|
|
292
|
+
throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview --write: ${arg}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
stage: "final",
|
|
298
|
+
slug,
|
|
299
|
+
spec: await resolveSpecContent(rawSpec, cwd),
|
|
300
|
+
sessionId,
|
|
301
|
+
json: hasFlag(args, "--json"),
|
|
302
|
+
deliberate: hasFlag(args, "--deliberate"),
|
|
303
|
+
handoff: rawHandoff as "ralplan" | undefined,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
112
307
|
async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
|
|
113
308
|
const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
|
|
114
309
|
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
@@ -169,10 +364,62 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
|
|
|
169
364
|
thresholdSource,
|
|
170
365
|
sessionId,
|
|
171
366
|
idea,
|
|
367
|
+
language: resolveDeepInterviewLanguagePreference(idea),
|
|
172
368
|
json: hasFlag(args, "--json"),
|
|
173
369
|
};
|
|
174
370
|
}
|
|
175
371
|
|
|
372
|
+
export async function persistDeepInterviewSpec(
|
|
373
|
+
cwd: string,
|
|
374
|
+
resolved: ResolvedDeepInterviewSpecWriteArgs,
|
|
375
|
+
): Promise<PersistedDeepInterviewSpec> {
|
|
376
|
+
const specsDir = path.join(cwd, ".gjc", "specs");
|
|
377
|
+
await fs.mkdir(specsDir, { recursive: true });
|
|
378
|
+
const specPath = path.join(specsDir, `deep-interview-${resolved.slug}.md`);
|
|
379
|
+
const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
|
|
380
|
+
await fs.writeFile(specPath, content);
|
|
381
|
+
|
|
382
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
383
|
+
const createdAt = new Date().toISOString();
|
|
384
|
+
await fs.appendFile(
|
|
385
|
+
path.join(specsDir, "deep-interview-index.jsonl"),
|
|
386
|
+
`${JSON.stringify({ slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 })}\n`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
390
|
+
const existing = await readJsonObject(statePath);
|
|
391
|
+
const payload: Record<string, unknown> = {
|
|
392
|
+
...existing,
|
|
393
|
+
active: true,
|
|
394
|
+
current_phase: "handoff",
|
|
395
|
+
skill: "deep-interview",
|
|
396
|
+
version: typeof existing.version === "number" ? existing.version : 1,
|
|
397
|
+
spec_slug: resolved.slug,
|
|
398
|
+
spec_path: specPath,
|
|
399
|
+
spec_sha256: sha256,
|
|
400
|
+
spec_stage: resolved.stage,
|
|
401
|
+
spec_persisted_at: createdAt,
|
|
402
|
+
updated_at: createdAt,
|
|
403
|
+
};
|
|
404
|
+
if (resolved.sessionId) payload.session_id = resolved.sessionId;
|
|
405
|
+
await writeJsonAtomic(statePath, payload);
|
|
406
|
+
await syncDeepInterviewHud({
|
|
407
|
+
cwd,
|
|
408
|
+
sessionId: resolved.sessionId,
|
|
409
|
+
phase: "handoff",
|
|
410
|
+
specStatus: "persisted",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
slug: resolved.slug,
|
|
415
|
+
path: specPath,
|
|
416
|
+
stage: resolved.stage,
|
|
417
|
+
sha256,
|
|
418
|
+
createdAt,
|
|
419
|
+
statePath,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
176
423
|
async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
|
|
177
424
|
const stateDir = resolved.sessionId
|
|
178
425
|
? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
|
|
@@ -196,6 +443,10 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
|
|
|
196
443
|
},
|
|
197
444
|
updated_at: now,
|
|
198
445
|
};
|
|
446
|
+
if (resolved.language) {
|
|
447
|
+
payload.language = resolved.language;
|
|
448
|
+
(payload.state as Record<string, unknown>).language = resolved.language;
|
|
449
|
+
}
|
|
199
450
|
if (resolved.sessionId) payload.session_id = resolved.sessionId;
|
|
200
451
|
await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
201
452
|
return statePath;
|
|
@@ -232,11 +483,70 @@ async function syncDeepInterviewHud(options: {
|
|
|
232
483
|
}
|
|
233
484
|
}
|
|
234
485
|
|
|
486
|
+
async function handleSpecWrite(args: readonly string[], cwd: string): Promise<DeepInterviewCommandResult> {
|
|
487
|
+
const resolved = await resolveSpecWriteArgs(args, cwd);
|
|
488
|
+
const persisted = await persistDeepInterviewSpec(cwd, resolved);
|
|
489
|
+
const shouldHandoff = resolved.deliberate || resolved.handoff === "ralplan";
|
|
490
|
+
const summary: DeepInterviewSpecWriteSummary = {
|
|
491
|
+
skill: "deep-interview",
|
|
492
|
+
stage: persisted.stage,
|
|
493
|
+
slug: persisted.slug,
|
|
494
|
+
path: persisted.path,
|
|
495
|
+
sha256: persisted.sha256,
|
|
496
|
+
created_at: persisted.createdAt,
|
|
497
|
+
state_path: persisted.statePath,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if (shouldHandoff) {
|
|
501
|
+
const ralplanArgs = ["--deliberate", "--json"];
|
|
502
|
+
if (resolved.sessionId) ralplanArgs.push("--session-id", resolved.sessionId);
|
|
503
|
+
ralplanArgs.push(persisted.path);
|
|
504
|
+
const ralplanResult = await runNativeRalplanCommand(ralplanArgs, cwd);
|
|
505
|
+
if (ralplanResult.status !== 0) {
|
|
506
|
+
throw new DeepInterviewCommandError(
|
|
507
|
+
ralplanResult.status,
|
|
508
|
+
ralplanResult.stderr?.trim() || "failed to seed ralplan",
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const handoffArgs = ["handoff", "--mode", "deep-interview", "--to", "ralplan", "--json"];
|
|
513
|
+
if (resolved.sessionId) handoffArgs.push("--session-id", resolved.sessionId);
|
|
514
|
+
else handoffArgs.push("--session-id", "");
|
|
515
|
+
const handoffResult = await runNativeStateCommand(handoffArgs, cwd);
|
|
516
|
+
if (handoffResult.status !== 0) {
|
|
517
|
+
throw new DeepInterviewCommandError(
|
|
518
|
+
handoffResult.status,
|
|
519
|
+
handoffResult.stderr?.trim() || "failed to hand off deep-interview to ralplan",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const ralplanPayload = ralplanResult.stdout ? (JSON.parse(ralplanResult.stdout) as Record<string, unknown>) : {};
|
|
524
|
+
summary.handoff = {
|
|
525
|
+
to: "ralplan",
|
|
526
|
+
mode: "deliberate",
|
|
527
|
+
state_path: typeof ralplanPayload.state_path === "string" ? ralplanPayload.state_path : undefined,
|
|
528
|
+
run_id: typeof ralplanPayload.run_id === "string" ? ralplanPayload.run_id : undefined,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const stdout = resolved.json
|
|
533
|
+
? `${JSON.stringify(summary, null, 2)}\n`
|
|
534
|
+
: [
|
|
535
|
+
`Persisted deep-interview ${persisted.stage} spec at ${persisted.path}.`,
|
|
536
|
+
shouldHandoff ? "Handed off deep-interview to ralplan (deliberate)." : undefined,
|
|
537
|
+
"",
|
|
538
|
+
]
|
|
539
|
+
.filter((line): line is string => Boolean(line))
|
|
540
|
+
.join("\n");
|
|
541
|
+
return { status: 0, stdout };
|
|
542
|
+
}
|
|
543
|
+
|
|
235
544
|
export async function runNativeDeepInterviewCommand(
|
|
236
545
|
args: string[],
|
|
237
546
|
cwd = process.cwd(),
|
|
238
547
|
): Promise<DeepInterviewCommandResult> {
|
|
239
548
|
try {
|
|
549
|
+
if (isDeepInterviewSpecWriteInvocation(args)) return await handleSpecWrite(args, cwd);
|
|
240
550
|
const resolved = await resolveDeepInterviewArgs(args, cwd);
|
|
241
551
|
if (!resolved.idea) {
|
|
242
552
|
throw new DeepInterviewCommandError(
|
|
@@ -260,6 +570,7 @@ export async function runNativeDeepInterviewCommand(
|
|
|
260
570
|
threshold: resolved.threshold,
|
|
261
571
|
threshold_source: resolved.thresholdSource,
|
|
262
572
|
idea: resolved.idea,
|
|
573
|
+
language: resolved.language,
|
|
263
574
|
state_path: statePath,
|
|
264
575
|
handoff: "Run `/skill:deep-interview` inside the GJC agent to drive the Socratic interview loop.",
|
|
265
576
|
};
|