@gajae-code/coding-agent 0.2.0 → 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.
- package/CHANGELOG.md +38 -1
- package/dist/types/cli/skills-cli.d.ts +9 -0
- package/dist/types/commands/contribution-prep.d.ts +18 -0
- package/dist/types/commands/session.d.ts +24 -0
- package/dist/types/commands/skills.d.ts +26 -0
- package/dist/types/config/model-registry.d.ts +33 -4
- package/dist/types/config/models-config-schema.d.ts +52 -5
- package/dist/types/config/settings-schema.d.ts +1 -24
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
- package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
- package/dist/types/goals/runtime.d.ts +3 -9
- package/dist/types/goals/state.d.ts +3 -6
- package/dist/types/goals/tools/goal-tool.d.ts +1 -69
- package/dist/types/modes/components/model-selector.d.ts +21 -1
- package/dist/types/modes/components/status-line/types.d.ts +0 -3
- package/dist/types/modes/components/status-line.d.ts +0 -3
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -12
- package/dist/types/modes/theme/defaults/index.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +1 -2
- package/dist/types/modes/types.d.ts +1 -7
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/contribution-prep.d.ts +47 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
- package/dist/types/skill-state/workflow-hud.d.ts +9 -4
- package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +17 -2
- package/src/cli/skills-cli.ts +88 -0
- package/src/cli.ts +7 -1
- package/src/commands/contribution-prep.ts +41 -0
- package/src/commands/deep-interview.ts +6 -22
- package/src/commands/launch.ts +10 -1
- package/src/commands/ralplan.ts +10 -22
- package/src/commands/session.ts +150 -0
- package/src/commands/skills.ts +48 -0
- package/src/commands/state.ts +14 -4
- package/src/commands/team.ts +23 -3
- package/src/commit/agentic/index.ts +1 -0
- package/src/commit/pipeline.ts +1 -0
- package/src/config/model-registry.ts +269 -10
- package/src/config/models-config-schema.ts +124 -88
- package/src/config/settings-schema.ts +1 -25
- package/src/config.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
- package/src/defaults/gjc/skills/team/SKILL.md +29 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
- package/src/eval/py/prelude.py +1 -1
- package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
- package/src/gjc-runtime/goal-mode-request.ts +2 -19
- package/src/gjc-runtime/launch-tmux.ts +83 -43
- package/src/gjc-runtime/ralplan-runtime.ts +460 -0
- package/src/gjc-runtime/state-runtime.ts +562 -0
- package/src/gjc-runtime/team-runtime.ts +708 -52
- package/src/gjc-runtime/tmux-common.ts +119 -0
- package/src/gjc-runtime/tmux-sessions.ts +165 -0
- package/src/gjc-runtime/ultragoal-guard.ts +6 -3
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
- package/src/goals/runtime.ts +38 -144
- package/src/goals/state.ts +36 -7
- package/src/goals/tools/goal-tool.ts +15 -172
- package/src/hooks/skill-state.ts +31 -12
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +10 -1
- package/src/modes/components/model-selector.ts +109 -28
- package/src/modes/components/skill-hud/render.ts +4 -0
- package/src/modes/components/status-line/segments.ts +5 -16
- package/src/modes/components/status-line/types.ts +0 -3
- package/src/modes/components/status-line.ts +0 -6
- package/src/modes/controllers/command-controller.ts +25 -1
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/selector-controller.ts +42 -2
- package/src/modes/interactive-mode.ts +18 -219
- package/src/modes/theme/defaults/dark-poimandres.json +0 -1
- package/src/modes/theme/defaults/light-poimandres.json +0 -1
- package/src/modes/theme/theme.ts +0 -6
- package/src/modes/types.ts +1 -7
- package/src/prompts/goals/goal-continuation.md +1 -4
- package/src/prompts/goals/goal-mode-active.md +3 -5
- package/src/prompts/system/system-prompt.md +5 -7
- package/src/prompts/tools/goal.md +4 -4
- package/src/sdk.ts +2 -1
- package/src/session/agent-session.ts +18 -0
- package/src/session/contribution-prep.ts +320 -0
- package/src/setup/provider-onboarding.ts +2 -0
- package/src/skill-state/active-state.ts +38 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
- package/src/skill-state/workflow-hud.ts +23 -5
- package/src/skill-state/workflow-state-contract.ts +121 -0
- package/src/slash-commands/acp-builtins.ts +11 -2
- package/src/slash-commands/builtin-registry.ts +40 -13
- package/src/task/commands.ts +1 -5
- package/src/tools/gh.ts +212 -2
- package/src/tools/index.ts +2 -5
- package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
- package/dist/types/commands/question.d.ts +0 -7
- package/dist/types/modes/loop-limit.d.ts +0 -22
- package/src/commands/gjc-runtime-bridge.ts +0 -227
- package/src/commands/question.ts +0 -12
- package/src/modes/loop-limit.ts +0 -140
- package/src/prompts/commands/orchestrate.md +0 -49
- package/src/prompts/goals/goal-budget-limit.md +0 -16
- package/src/prompts/tools/create-goal.md +0 -3
- package/src/prompts/tools/get-goal.md +0 -3
- package/src/prompts/tools/update-goal.md +0 -3
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
5
|
+
import type { AssistantMessage, ToolResultMessage, UserMessage } from "@gajae-code/ai";
|
|
6
|
+
import { $ } from "bun";
|
|
7
|
+
import { resolveGjcCommand } from "../task/gjc-command";
|
|
8
|
+
import { shortenPath } from "../tools/render-utils";
|
|
9
|
+
|
|
10
|
+
export const CONTRIBUTION_PREP_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
const MAX_TRANSCRIPT_MESSAGES = 20;
|
|
13
|
+
const MAX_TEXT_CHARS = 12000;
|
|
14
|
+
const MAX_GIT_OUTPUT_CHARS = 60000;
|
|
15
|
+
|
|
16
|
+
export interface ContributionPrepArtifact {
|
|
17
|
+
path: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ContributionPrepManifest {
|
|
22
|
+
schema_version: number;
|
|
23
|
+
source_session_id: string;
|
|
24
|
+
created_at: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
git_head: string | null;
|
|
27
|
+
changed_files: string[];
|
|
28
|
+
artifacts: ContributionPrepArtifact[];
|
|
29
|
+
redactions: string[];
|
|
30
|
+
recommended_output: string[];
|
|
31
|
+
worker_prompt_path: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ContributionPrepResult {
|
|
35
|
+
manifestPath: string;
|
|
36
|
+
workerPromptPath: string;
|
|
37
|
+
artifactDir: string;
|
|
38
|
+
changedFiles: string[];
|
|
39
|
+
spawned: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ContributionPrepOptions {
|
|
43
|
+
customInstructions?: string;
|
|
44
|
+
spawnWorker?: boolean;
|
|
45
|
+
artifactRoot?: string;
|
|
46
|
+
now?: Date;
|
|
47
|
+
spawn?: (args: string[], cwd: string, shell: boolean) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ContributionPrepContext {
|
|
51
|
+
sessionId: string;
|
|
52
|
+
cwd: string;
|
|
53
|
+
sessionFile?: string;
|
|
54
|
+
messages: AgentMessage[];
|
|
55
|
+
customInstructions?: string;
|
|
56
|
+
now?: Date;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RedactionState {
|
|
60
|
+
labels: Set<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function limitText(text: string, maxChars = MAX_TEXT_CHARS): string {
|
|
64
|
+
if (text.length <= maxChars) return text;
|
|
65
|
+
return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} chars]`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function replaceRegex(text: string, regex: RegExp, replacement: string, state: RedactionState, label: string): string {
|
|
69
|
+
if (!regex.test(text)) return text;
|
|
70
|
+
state.labels.add(label);
|
|
71
|
+
regex.lastIndex = 0;
|
|
72
|
+
return text.replace(regex, replacement);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function redactContributionPrepText(
|
|
76
|
+
text: string,
|
|
77
|
+
cwd: string,
|
|
78
|
+
state: RedactionState = { labels: new Set() },
|
|
79
|
+
): string {
|
|
80
|
+
let redacted = text;
|
|
81
|
+
redacted = replaceRegex(
|
|
82
|
+
redacted,
|
|
83
|
+
/\b(?:sk|pk|rk|xox[baprs])-[-_A-Za-z0-9]{12,}\b/g,
|
|
84
|
+
"[REDACTED_TOKEN]",
|
|
85
|
+
state,
|
|
86
|
+
"tokens",
|
|
87
|
+
);
|
|
88
|
+
redacted = replaceRegex(
|
|
89
|
+
redacted,
|
|
90
|
+
/\b(?:ghp_[A-Za-z0-9_]{12,}|gho_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})\b/g,
|
|
91
|
+
"[REDACTED_TOKEN]",
|
|
92
|
+
state,
|
|
93
|
+
"tokens",
|
|
94
|
+
);
|
|
95
|
+
redacted = replaceRegex(
|
|
96
|
+
redacted,
|
|
97
|
+
/\b((?:ANTHROPIC|OPENAI|GITHUB|GOOGLE|GEMINI|KAGI|TAVILY|EXA|PERPLEXITY|ZAI|KIMI|BRAVE|SEARXNG|AWS)_[A-Z0-9_]*(?:KEY|TOKEN|SECRET|COOKIE|PASSWORD))\s*=\s*[^\s\n]+/gi,
|
|
98
|
+
"$1=[REDACTED_SECRET]",
|
|
99
|
+
state,
|
|
100
|
+
"provider_keys",
|
|
101
|
+
);
|
|
102
|
+
redacted = replaceRegex(
|
|
103
|
+
redacted,
|
|
104
|
+
/\b(Authorization|Proxy-Authorization)\s*:\s*(?:Bearer|Basic|Token)\s+[^\s\n]+/gi,
|
|
105
|
+
"$1: [REDACTED_AUTH_HEADER]",
|
|
106
|
+
state,
|
|
107
|
+
"auth_headers",
|
|
108
|
+
);
|
|
109
|
+
redacted = replaceRegex(redacted, /\b(Cookie|Set-Cookie)\s*:\s*[^\n]+/gi, "$1: [REDACTED_COOKIE]", state, "cookies");
|
|
110
|
+
redacted = replaceRegex(
|
|
111
|
+
redacted,
|
|
112
|
+
/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})[^\s)>'"]*/gi,
|
|
113
|
+
"[REDACTED_PRIVATE_ENDPOINT]",
|
|
114
|
+
state,
|
|
115
|
+
"private_endpoints",
|
|
116
|
+
);
|
|
117
|
+
const home = os.homedir();
|
|
118
|
+
if (home && redacted.includes(home)) {
|
|
119
|
+
state.labels.add("home_paths");
|
|
120
|
+
redacted = redacted.split(home).join("~");
|
|
121
|
+
}
|
|
122
|
+
const normalizedCwd = path.resolve(cwd);
|
|
123
|
+
if (normalizedCwd && redacted.includes(normalizedCwd)) {
|
|
124
|
+
state.labels.add("cwd_paths");
|
|
125
|
+
redacted = redacted.split(normalizedCwd).join(shortenPath(normalizedCwd));
|
|
126
|
+
}
|
|
127
|
+
return redacted;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function contentText(content: UserMessage["content"] | AssistantMessage["content"]): string {
|
|
131
|
+
if (typeof content === "string") return content;
|
|
132
|
+
return content
|
|
133
|
+
.map(part => {
|
|
134
|
+
if (part.type === "text") return part.text;
|
|
135
|
+
if (part.type === "toolCall") return `[tool call: ${part.name}] ${JSON.stringify(part.arguments)}`;
|
|
136
|
+
if (part.type === "image") return "[image]";
|
|
137
|
+
return `[${part.type}]`;
|
|
138
|
+
})
|
|
139
|
+
.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatMessage(message: AgentMessage): string {
|
|
143
|
+
if (message.role === "user" || message.role === "assistant") {
|
|
144
|
+
return `## ${message.role}\n\n${contentText(message.content)}\n`;
|
|
145
|
+
}
|
|
146
|
+
if (message.role === "toolResult") {
|
|
147
|
+
const tool = message as ToolResultMessage;
|
|
148
|
+
return `## toolResult: ${tool.toolName}\n\n${typeof tool.content === "string" ? tool.content : JSON.stringify(tool.content)}\n`;
|
|
149
|
+
}
|
|
150
|
+
return `## ${message.role}\n\n${JSON.stringify(message)}\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function gitOutput(cwd: string, args: string[], maxChars = MAX_GIT_OUTPUT_CHARS): Promise<string> {
|
|
154
|
+
try {
|
|
155
|
+
const output = await $`git ${args}`.cwd(cwd).quiet().text();
|
|
156
|
+
return limitText(output.trim(), maxChars);
|
|
157
|
+
} catch {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function changedFiles(cwd: string): Promise<string[]> {
|
|
163
|
+
const output = await gitOutput(cwd, ["status", "--short"]);
|
|
164
|
+
return output
|
|
165
|
+
.split("\n")
|
|
166
|
+
.map(line => line.trim())
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.map(line => line.replace(/^..\s+/, ""));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function writeArtifact(
|
|
172
|
+
dir: string,
|
|
173
|
+
name: string,
|
|
174
|
+
description: string,
|
|
175
|
+
text: string,
|
|
176
|
+
): Promise<ContributionPrepArtifact> {
|
|
177
|
+
const filePath = path.join(dir, name);
|
|
178
|
+
await Bun.write(filePath, `${text.trimEnd()}\n`);
|
|
179
|
+
return { path: filePath, description };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function buildContributionPrepWorkerPrompt(manifestPath: string): string {
|
|
183
|
+
return [
|
|
184
|
+
"Prepare a maintainer-friendly contribution draft from the redacted context dump.",
|
|
185
|
+
"Read the manifest and referenced artifact file pointers. Do not assume transcript context was inlined here.",
|
|
186
|
+
`Manifest: ${manifestPath}`,
|
|
187
|
+
"Produce structured markdown with: title, problem summary, reproduction/context, proposed fix or implementation plan, affected files, tests to run, and uncertainty/remaining risks.",
|
|
188
|
+
"Do not create GitHub issues, open PRs, push branches, or perform remote writes unless the user explicitly confirms that action in this fresh session.",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function prepareContributionPrep(
|
|
193
|
+
context: ContributionPrepContext,
|
|
194
|
+
options: ContributionPrepOptions = {},
|
|
195
|
+
): Promise<ContributionPrepResult> {
|
|
196
|
+
const createdAt = (options.now ?? context.now ?? new Date()).toISOString();
|
|
197
|
+
const safeTimestamp = createdAt.replace(/[:.]/g, "-");
|
|
198
|
+
const artifactDir = path.join(
|
|
199
|
+
options.artifactRoot ?? path.join(context.cwd, ".gjc", "contribution-prep"),
|
|
200
|
+
safeTimestamp,
|
|
201
|
+
);
|
|
202
|
+
await fs.mkdir(artifactDir, { recursive: true });
|
|
203
|
+
|
|
204
|
+
const redactions: RedactionState = { labels: new Set() };
|
|
205
|
+
const recentMessages = context.messages.slice(-MAX_TRANSCRIPT_MESSAGES);
|
|
206
|
+
const artifacts: ContributionPrepArtifact[] = [];
|
|
207
|
+
const redact = (text: string) => redactContributionPrepText(text, context.cwd, redactions);
|
|
208
|
+
|
|
209
|
+
artifacts.push(
|
|
210
|
+
await writeArtifact(
|
|
211
|
+
artifactDir,
|
|
212
|
+
"transcript.md",
|
|
213
|
+
"Redacted recent transcript window",
|
|
214
|
+
redact(recentMessages.map(formatMessage).join("\n---\n")),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
artifacts.push(
|
|
218
|
+
await writeArtifact(
|
|
219
|
+
artifactDir,
|
|
220
|
+
"summary.md",
|
|
221
|
+
"Current session summary and operator instructions",
|
|
222
|
+
redact(
|
|
223
|
+
[
|
|
224
|
+
`# Contribution prep context`,
|
|
225
|
+
`Source session: ${context.sessionId}`,
|
|
226
|
+
`Session file: ${context.sessionFile ?? "(none)"}`,
|
|
227
|
+
`Working directory: ${context.cwd}`,
|
|
228
|
+
options.customInstructions || context.customInstructions
|
|
229
|
+
? `Custom instructions: ${options.customInstructions ?? context.customInstructions}`
|
|
230
|
+
: "Custom instructions: (none)",
|
|
231
|
+
].join("\n"),
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const gitHead = (await gitOutput(context.cwd, ["rev-parse", "HEAD"])) || null;
|
|
237
|
+
const files = await changedFiles(context.cwd);
|
|
238
|
+
artifacts.push(
|
|
239
|
+
await writeArtifact(artifactDir, "changed-files.txt", "Changed files from git status", redact(files.join("\n"))),
|
|
240
|
+
);
|
|
241
|
+
artifacts.push(
|
|
242
|
+
await writeArtifact(
|
|
243
|
+
artifactDir,
|
|
244
|
+
"git-diff.patch",
|
|
245
|
+
"Bounded redacted git diff",
|
|
246
|
+
redact(await gitOutput(context.cwd, ["diff", "--no-ext-diff"])),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
artifacts.push(
|
|
250
|
+
await writeArtifact(
|
|
251
|
+
artifactDir,
|
|
252
|
+
"environment.md",
|
|
253
|
+
"Redacted environment and reproduction metadata",
|
|
254
|
+
redact(
|
|
255
|
+
[
|
|
256
|
+
`cwd: ${context.cwd}`,
|
|
257
|
+
`git_head: ${gitHead ?? "unknown"}`,
|
|
258
|
+
`platform: ${process.platform}`,
|
|
259
|
+
`arch: ${process.arch}`,
|
|
260
|
+
`bun: ${Bun.version}`,
|
|
261
|
+
].join("\n"),
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const manifestPath = path.join(artifactDir, "manifest.json");
|
|
267
|
+
const workerPromptPath = path.join(artifactDir, "worker-prompt.md");
|
|
268
|
+
await Bun.write(workerPromptPath, `${buildContributionPrepWorkerPrompt(manifestPath)}\n`);
|
|
269
|
+
|
|
270
|
+
const manifest: ContributionPrepManifest = {
|
|
271
|
+
schema_version: CONTRIBUTION_PREP_SCHEMA_VERSION,
|
|
272
|
+
source_session_id: context.sessionId,
|
|
273
|
+
created_at: createdAt,
|
|
274
|
+
cwd: redact(context.cwd),
|
|
275
|
+
git_head: gitHead,
|
|
276
|
+
changed_files: files,
|
|
277
|
+
artifacts,
|
|
278
|
+
redactions: [...redactions.labels].sort(),
|
|
279
|
+
recommended_output: [
|
|
280
|
+
"title",
|
|
281
|
+
"problem summary",
|
|
282
|
+
"reproduction/context",
|
|
283
|
+
"proposed fix or implementation plan",
|
|
284
|
+
"affected files",
|
|
285
|
+
"tests to run",
|
|
286
|
+
"uncertainty / remaining risks",
|
|
287
|
+
],
|
|
288
|
+
worker_prompt_path: workerPromptPath,
|
|
289
|
+
};
|
|
290
|
+
await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
291
|
+
|
|
292
|
+
let spawned = false;
|
|
293
|
+
if (options.spawnWorker) {
|
|
294
|
+
const spawn =
|
|
295
|
+
options.spawn ??
|
|
296
|
+
(async (args, cwd, shell) => {
|
|
297
|
+
if (shell) {
|
|
298
|
+
Bun.spawn({
|
|
299
|
+
cmd: args,
|
|
300
|
+
cwd,
|
|
301
|
+
stdout: "inherit",
|
|
302
|
+
stderr: "inherit",
|
|
303
|
+
stdin: "inherit",
|
|
304
|
+
windowsVerbatimArguments: true,
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
Bun.spawn(args, { cwd, stdout: "inherit", stderr: "inherit", stdin: "inherit" });
|
|
309
|
+
});
|
|
310
|
+
const command = resolveGjcCommand();
|
|
311
|
+
await spawn(
|
|
312
|
+
[command.cmd, ...command.args, "--no-skills", "--", `@${workerPromptPath}`],
|
|
313
|
+
context.cwd,
|
|
314
|
+
command.shell,
|
|
315
|
+
);
|
|
316
|
+
spawned = true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { manifestPath, workerPromptPath, artifactDir, changedFiles: files, spawned };
|
|
320
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { getAgentDir } from "@gajae-code/utils";
|
|
3
4
|
import { YAML } from "bun";
|
|
@@ -131,6 +132,7 @@ async function writeModelsConfig(modelsPath: string, config: ModelsConfig): Prom
|
|
|
131
132
|
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
132
133
|
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
133
134
|
}
|
|
135
|
+
await fs.mkdir(path.dirname(modelsPath), { recursive: true });
|
|
134
136
|
await Bun.write(modelsPath, YAML.stringify(checked.data, null, 2));
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
3
4
|
|
|
4
5
|
export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
5
6
|
export const SKILL_ACTIVE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -25,6 +26,8 @@ export interface WorkflowHudSummary {
|
|
|
25
26
|
updated_at?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
30
|
+
|
|
28
31
|
export interface SkillActiveEntry {
|
|
29
32
|
skill: string;
|
|
30
33
|
phase?: string;
|
|
@@ -36,6 +39,7 @@ export interface SkillActiveEntry {
|
|
|
36
39
|
turn_id?: string;
|
|
37
40
|
hud?: WorkflowHudSummary;
|
|
38
41
|
stale?: boolean;
|
|
42
|
+
receipt?: WorkflowStateReceipt;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface SkillActiveState {
|
|
@@ -70,6 +74,7 @@ export interface SyncSkillActiveStateOptions {
|
|
|
70
74
|
nowIso?: string;
|
|
71
75
|
source?: string;
|
|
72
76
|
hud?: WorkflowHudSummary;
|
|
77
|
+
receipt?: WorkflowStateReceipt;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
const HUD_TEXT_LIMIT = 80;
|
|
@@ -142,6 +147,36 @@ export function normalizeWorkflowHudSummary(raw: unknown): WorkflowHudSummary |
|
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
function normalizeWorkflowStateReceipt(raw: unknown): WorkflowStateReceipt | undefined {
|
|
151
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
152
|
+
const record = raw as Record<string, unknown>;
|
|
153
|
+
if (record.version !== 1) return undefined;
|
|
154
|
+
const skill = safeString(record.skill).trim();
|
|
155
|
+
if (!isCanonicalGjcWorkflowSkill(skill)) return undefined;
|
|
156
|
+
const owner = safeString(record.owner).trim();
|
|
157
|
+
if (owner !== "gjc-state-cli" && owner !== "gjc-runtime" && owner !== "gjc-hook") return undefined;
|
|
158
|
+
const command = sanitizeHudString(record.command, 120);
|
|
159
|
+
const statePath = sanitizeHudString(record.state_path, 240);
|
|
160
|
+
const storagePath = sanitizeHudString(record.storage_path, 240);
|
|
161
|
+
const mutatedAt = sanitizeHudString(record.mutated_at, 40);
|
|
162
|
+
const freshUntil = sanitizeHudString(record.fresh_until, 40);
|
|
163
|
+
const status = safeString(record.status).trim();
|
|
164
|
+
const mutationId = sanitizeHudString(record.mutation_id, 120);
|
|
165
|
+
if (!command || !statePath || !storagePath || !mutatedAt || !freshUntil || !mutationId) return undefined;
|
|
166
|
+
return {
|
|
167
|
+
version: 1,
|
|
168
|
+
skill,
|
|
169
|
+
owner,
|
|
170
|
+
command,
|
|
171
|
+
state_path: statePath,
|
|
172
|
+
storage_path: storagePath,
|
|
173
|
+
mutated_at: mutatedAt,
|
|
174
|
+
fresh_until: freshUntil,
|
|
175
|
+
status: status === "stale" ? "stale" : "fresh",
|
|
176
|
+
mutation_id: mutationId,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
145
180
|
function encodePathSegment(value: string): string {
|
|
146
181
|
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
147
182
|
}
|
|
@@ -175,6 +210,7 @@ function normalizeEntry(raw: unknown): SkillActiveEntry | null {
|
|
|
175
210
|
const skill = safeString(record.skill).trim();
|
|
176
211
|
if (!skill) return null;
|
|
177
212
|
const hud = normalizeWorkflowHudSummary(record.hud);
|
|
213
|
+
const receipt = normalizeWorkflowStateReceipt(record.receipt);
|
|
178
214
|
return {
|
|
179
215
|
...record,
|
|
180
216
|
skill,
|
|
@@ -186,6 +222,7 @@ function normalizeEntry(raw: unknown): SkillActiveEntry | null {
|
|
|
186
222
|
thread_id: safeString(record.thread_id).trim() || undefined,
|
|
187
223
|
turn_id: safeString(record.turn_id).trim() || undefined,
|
|
188
224
|
...(hud ? { hud } : {}),
|
|
225
|
+
...(receipt ? { receipt } : {}),
|
|
189
226
|
stale: undefined,
|
|
190
227
|
};
|
|
191
228
|
}
|
|
@@ -338,6 +375,7 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
338
375
|
thread_id: options.threadId,
|
|
339
376
|
turn_id: options.turnId,
|
|
340
377
|
...(hud ? { hud } : {}),
|
|
378
|
+
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
341
379
|
};
|
|
342
380
|
const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, options.sessionId);
|
|
343
381
|
const rootState = (await readStateFile(rootPath)) ?? { version: 1, active_skills: [] };
|
|
@@ -5,14 +5,20 @@ import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/lo
|
|
|
5
5
|
import { resolveToCwd } from "../tools/path-utils";
|
|
6
6
|
import { ToolError } from "../tools/tool-errors";
|
|
7
7
|
import { listActiveSkills, readVisibleSkillActiveState, type SkillActiveEntry } from "./active-state";
|
|
8
|
+
import {
|
|
9
|
+
type CanonicalGjcWorkflowSkill,
|
|
10
|
+
sanctionedWorkflowStateCommand,
|
|
11
|
+
workflowModeStateFileName,
|
|
12
|
+
} from "./workflow-state-contract";
|
|
8
13
|
|
|
9
14
|
export const DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE =
|
|
10
|
-
"Deep-interview is active;
|
|
15
|
+
"Deep-interview is active; continue interviewing with `ask`, write/finalize pending specs through the required GJC workflow CLI, or use an explicit force override. Direct `.gjc/` and product-code edits are blocked until explicit execution approval.";
|
|
16
|
+
export const WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE =
|
|
17
|
+
"Workflow state JSON is runtime-owned. Use `gjc state <skill> read|write --input '<json>'` for deep-interview, ralplan, ultragoal, and team. Planning artifacts under `.gjc/specs/` and `.gjc/plans/` remain allowed.";
|
|
11
18
|
|
|
12
19
|
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
|
|
13
20
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
14
21
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
15
|
-
const GLOB_META_RE = /[*?[\]{}]/;
|
|
16
22
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
17
23
|
|
|
18
24
|
type ToolWithEditMode = AgentTool & {
|
|
@@ -26,6 +32,8 @@ export interface DeepInterviewMutationGuardInput {
|
|
|
26
32
|
threadId?: string;
|
|
27
33
|
tool: ToolWithEditMode;
|
|
28
34
|
args: unknown;
|
|
35
|
+
forceOverride?: boolean;
|
|
36
|
+
enforceWorkflowState?: boolean;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
interface ExtractedTargets {
|
|
@@ -38,6 +46,7 @@ export interface DeepInterviewMutationDecision {
|
|
|
38
46
|
message?: string;
|
|
39
47
|
targets: string[];
|
|
40
48
|
reason?: string;
|
|
49
|
+
command?: string;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
interface ModeState {
|
|
@@ -246,34 +255,67 @@ function resolveRawPath(cwd: string, rawPath: string): { absolutePath?: string;
|
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
function
|
|
258
|
+
function relativeGjcSegments(cwd: string, rawPath: string): string[] | null {
|
|
250
259
|
const { absolutePath, unknown } = resolveRawPath(cwd, rawPath);
|
|
251
|
-
if (unknown || !absolutePath) return
|
|
260
|
+
if (unknown || !absolutePath) return null;
|
|
252
261
|
const relative = path.relative(path.resolve(cwd), path.resolve(absolutePath));
|
|
253
|
-
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return
|
|
254
|
-
|
|
255
|
-
return segments[0] === ".gjc" && (segments[1] === "specs" || segments[1] === "state");
|
|
262
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
|
263
|
+
return normalizePosix(relative).split("/").filter(Boolean);
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
function
|
|
266
|
+
function blockedWorkflowStateSkill(cwd: string, rawPath: string): CanonicalGjcWorkflowSkill | null {
|
|
267
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
268
|
+
if (!segments || segments[0] !== ".gjc") return null;
|
|
269
|
+
if (segments[1] === "specs" || segments[1] === "plans") return null;
|
|
270
|
+
if (segments[1] !== "state") return null;
|
|
271
|
+
const fileName = segments.at(-1) ?? "";
|
|
272
|
+
for (const skillName of ["deep-interview", "ralplan", "ultragoal", "team"] as const) {
|
|
273
|
+
if (fileName === workflowModeStateFileName(skillName)) return skillName;
|
|
274
|
+
}
|
|
275
|
+
if (fileName === "skill-active-state.json") return "deep-interview";
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function firstBlockedWorkflowStateSkill(cwd: string, targets: ExtractedTargets): CanonicalGjcWorkflowSkill | null {
|
|
280
|
+
for (const rawPath of targets.paths) {
|
|
281
|
+
const skill = blockedWorkflowStateSkill(cwd, rawPath);
|
|
282
|
+
if (skill) return skill;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isGjcManagedPath(cwd: string, rawPath: string): boolean {
|
|
288
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
289
|
+
return segments?.[0] === ".gjc";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isAllowlistedPath(cwd: string, rawPath: string): boolean {
|
|
293
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
294
|
+
if (!segments || segments[0] !== ".gjc") return false;
|
|
295
|
+
return segments[1] === "specs" || segments[1] === "plans";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function hasGjcManagedTarget(cwd: string, targets: ExtractedTargets): boolean {
|
|
259
299
|
if (targets.unknown || targets.paths.length === 0) return false;
|
|
260
|
-
return targets.paths.
|
|
261
|
-
if (GLOB_META_RE.test(rawPath)) {
|
|
262
|
-
return isAllowlistedPath(cwd, rawPath);
|
|
263
|
-
}
|
|
264
|
-
return isAllowlistedPath(cwd, rawPath);
|
|
265
|
-
});
|
|
300
|
+
return targets.paths.some(rawPath => isGjcManagedPath(cwd, rawPath));
|
|
266
301
|
}
|
|
267
302
|
|
|
303
|
+
function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets): boolean {
|
|
304
|
+
return (
|
|
305
|
+
!targets.unknown && targets.paths.length > 0 && targets.paths.every(rawPath => isAllowlistedPath(cwd, rawPath))
|
|
306
|
+
);
|
|
307
|
+
}
|
|
268
308
|
export async function assertDeepInterviewMutationRawPathsAllowed(input: {
|
|
269
309
|
cwd: string;
|
|
270
310
|
sessionId?: string;
|
|
271
311
|
threadId?: string;
|
|
272
312
|
rawPaths: string[];
|
|
313
|
+
forceOverride?: boolean;
|
|
273
314
|
}): Promise<void> {
|
|
315
|
+
if (input.forceOverride) return;
|
|
274
316
|
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) return;
|
|
275
317
|
const targets: ExtractedTargets = { paths: input.rawPaths, unknown: input.rawPaths.length === 0 };
|
|
276
|
-
if (
|
|
318
|
+
if (hasGjcManagedTarget(input.cwd, targets)) {
|
|
277
319
|
throw new ToolError(DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE);
|
|
278
320
|
}
|
|
279
321
|
}
|
|
@@ -282,19 +324,41 @@ export async function getDeepInterviewMutationDecision(
|
|
|
282
324
|
input: DeepInterviewMutationGuardInput,
|
|
283
325
|
): Promise<DeepInterviewMutationDecision> {
|
|
284
326
|
if (!BLOCKED_TOOL_NAMES.has(input.tool.name)) return { blocked: false, targets: [] };
|
|
327
|
+
const targets = extractTargets(input.tool, input.args);
|
|
328
|
+
if (input.enforceWorkflowState !== false) {
|
|
329
|
+
const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
|
|
330
|
+
if (stateSkill) {
|
|
331
|
+
const command = sanctionedWorkflowStateCommand(stateSkill);
|
|
332
|
+
return {
|
|
333
|
+
blocked: true,
|
|
334
|
+
message: `${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`,
|
|
335
|
+
targets: targets.paths,
|
|
336
|
+
reason: "workflow-state-target",
|
|
337
|
+
command,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
285
341
|
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) {
|
|
286
342
|
return { blocked: false, targets: [] };
|
|
287
343
|
}
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
return {
|
|
344
|
+
if (input.forceOverride) return { blocked: false, targets: [] };
|
|
345
|
+
if (targets.unknown) {
|
|
346
|
+
return {
|
|
347
|
+
blocked: true,
|
|
348
|
+
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
349
|
+
targets: targets.paths,
|
|
350
|
+
reason: "unknown-target",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (hasGjcManagedTarget(input.cwd, targets) && !allTargetsAllowlisted(input.cwd, targets)) {
|
|
354
|
+
return {
|
|
355
|
+
blocked: true,
|
|
356
|
+
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
357
|
+
targets: targets.paths,
|
|
358
|
+
reason: "gjc-managed-target",
|
|
359
|
+
};
|
|
291
360
|
}
|
|
292
|
-
return {
|
|
293
|
-
blocked: true,
|
|
294
|
-
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
295
|
-
targets: targets.paths,
|
|
296
|
-
reason: targets.unknown ? "unknown-target" : "product-target",
|
|
297
|
-
};
|
|
361
|
+
return { blocked: false, targets: targets.paths };
|
|
298
362
|
}
|
|
299
363
|
|
|
300
364
|
export async function assertDeepInterviewMutationAllowed(input: DeepInterviewMutationGuardInput): Promise<void> {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { WorkflowHudChip, WorkflowHudSummary } from "./active-state";
|
|
2
2
|
|
|
3
|
-
interface
|
|
3
|
+
interface WorkflowGateHudState {
|
|
4
|
+
approvalStatus?: string;
|
|
5
|
+
blockedReason?: string;
|
|
6
|
+
nextAction?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DeepInterviewHudState extends WorkflowGateHudState {
|
|
4
10
|
phase?: string;
|
|
5
11
|
ambiguity?: number;
|
|
6
12
|
threshold?: number;
|
|
@@ -11,7 +17,7 @@ interface DeepInterviewHudState {
|
|
|
11
17
|
updatedAt?: string;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
interface RalplanHudState {
|
|
20
|
+
interface RalplanHudState extends WorkflowGateHudState {
|
|
15
21
|
stage?: string;
|
|
16
22
|
waiting?: string;
|
|
17
23
|
iteration?: number;
|
|
@@ -27,7 +33,7 @@ interface UltragoalLikeGoal {
|
|
|
27
33
|
status: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
interface UltragoalHudState {
|
|
36
|
+
interface UltragoalHudState extends WorkflowGateHudState {
|
|
31
37
|
status: string;
|
|
32
38
|
currentGoal?: UltragoalLikeGoal;
|
|
33
39
|
counts: Record<string, number>;
|
|
@@ -41,7 +47,7 @@ interface TeamHudWorker {
|
|
|
41
47
|
status?: string;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
interface TeamHudState {
|
|
50
|
+
interface TeamHudState extends WorkflowGateHudState {
|
|
45
51
|
phase: string;
|
|
46
52
|
task_total: number;
|
|
47
53
|
task_counts: Record<string, number>;
|
|
@@ -66,6 +72,14 @@ function chip(
|
|
|
66
72
|
return { label, value, priority, ...(severity ? { severity } : {}) };
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
function gateChips(state: WorkflowGateHudState, gatePriority: number): Array<WorkflowHudChip | null> {
|
|
76
|
+
return [
|
|
77
|
+
chip("gate", state.approvalStatus, gatePriority, state.approvalStatus === "approved" ? "success" : "warning"),
|
|
78
|
+
chip("blocked", state.blockedReason, gatePriority + 10, "blocked"),
|
|
79
|
+
chip("next", state.nextAction, gatePriority + 20),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function compactChips(chips: Array<WorkflowHudChip | null>): WorkflowHudChip[] {
|
|
70
84
|
return chips.filter((item): item is WorkflowHudChip => item !== null);
|
|
71
85
|
}
|
|
@@ -74,6 +88,7 @@ export function buildDeepInterviewHudSummary(state: DeepInterviewHudState): Work
|
|
|
74
88
|
return {
|
|
75
89
|
version: 1,
|
|
76
90
|
chips: compactChips([
|
|
91
|
+
...gateChips(state, 5),
|
|
77
92
|
chip("phase", state.phase, 10),
|
|
78
93
|
chip("ambiguity", [percent(state.ambiguity), percent(state.threshold)].filter(Boolean).join("/"), 20),
|
|
79
94
|
chip("round", state.roundCount === undefined ? undefined : String(state.roundCount), 30),
|
|
@@ -100,6 +115,7 @@ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSumma
|
|
|
100
115
|
summary: state.latestSummary,
|
|
101
116
|
chips: compactChips([
|
|
102
117
|
state.pendingApproval ? { label: "pending", value: "approval", priority: 5, severity: "warning" } : null,
|
|
118
|
+
...gateChips(state, 6),
|
|
103
119
|
chip("stage", state.stage, 10),
|
|
104
120
|
chip("waiting", state.waiting, 20),
|
|
105
121
|
chip("iter", state.iteration === undefined ? undefined : String(state.iteration), 30),
|
|
@@ -120,6 +136,7 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
|
|
|
120
136
|
chip("goals", `${complete}/${total}`, 10),
|
|
121
137
|
chip("current", state.currentGoal ? `${state.currentGoal.id}:${state.currentGoal.title}` : state.status, 20),
|
|
122
138
|
chip("status", state.status, 30, state.status === "complete" ? "success" : undefined),
|
|
139
|
+
...gateChips(state, 40),
|
|
123
140
|
]),
|
|
124
141
|
details: state.latestLedgerEvent
|
|
125
142
|
? compactChips([
|
|
@@ -153,7 +170,8 @@ export function buildTeamHudSummary(state: TeamHudState): WorkflowHudSummary {
|
|
|
153
170
|
chip("phase", state.phase, 10),
|
|
154
171
|
chip("workers", `${state.workers.length - failedWorkers}/${state.workers.length}`, 20),
|
|
155
172
|
chip("tasks", `${completed}/${state.task_total}`, 30),
|
|
156
|
-
|
|
173
|
+
...gateChips(state, 40),
|
|
174
|
+
chip("latest", latest, 70),
|
|
157
175
|
]),
|
|
158
176
|
...(state.updated_at ? { updated_at: state.updated_at } : {}),
|
|
159
177
|
};
|