@kynetic-ai/spec 0.7.0 → 0.9.0
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/dist/agents/adapters.d.ts +2 -0
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +18 -0
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/spawner.d.ts +2 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +4 -2
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts +48 -0
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +344 -86
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +8 -0
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/compact.d.ts +13 -0
- package/dist/cli/commands/session/compact.d.ts.map +1 -0
- package/dist/cli/commands/session/compact.js +207 -0
- package/dist/cli/commands/session/compact.js.map +1 -0
- package/dist/cli/commands/session/log.d.ts +2 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +12 -2
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/setup-seeding.d.ts +6 -3
- package/dist/cli/commands/setup-seeding.d.ts.map +1 -1
- package/dist/cli/commands/setup-seeding.js +20 -4
- package/dist/cli/commands/setup-seeding.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +3 -2
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +10 -90
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +104 -1
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/lib/codex-config.d.ts +14 -0
- package/dist/lib/codex-config.d.ts.map +1 -0
- package/dist/lib/codex-config.js +88 -0
- package/dist/lib/codex-config.js.map +1 -0
- package/dist/parser/agent-detection.d.ts +14 -0
- package/dist/parser/agent-detection.d.ts.map +1 -0
- package/dist/parser/agent-detection.js +118 -0
- package/dist/parser/agent-detection.js.map +1 -0
- package/dist/parser/setup-status.d.ts +4 -3
- package/dist/parser/setup-status.d.ts.map +1 -1
- package/dist/parser/setup-status.js +4 -10
- package/dist/parser/setup-status.js.map +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +22 -31
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +23 -1
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +126 -17
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/ralph/subagent.d.ts +2 -0
- package/dist/ralph/subagent.d.ts.map +1 -1
- package/dist/ralph/subagent.js +2 -0
- package/dist/ralph/subagent.js.map +1 -1
- package/dist/ralph/wrap-up.d.ts +2 -0
- package/dist/ralph/wrap-up.d.ts.map +1 -1
- package/dist/ralph/wrap-up.js +1 -0
- package/dist/ralph/wrap-up.js.map +1 -1
- package/dist/sessions/store.d.ts +67 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +396 -16
- package/dist/sessions/store.js.map +1 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +10 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +10 -0
- package/plugin/plugins/kspec/skills/review/SKILL.md +2 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +10 -0
- package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +1 -0
- package/templates/skills/create-workflow/SKILL.md +12 -2
- package/templates/skills/manifest.yaml +11 -0
- package/templates/skills/observations/SKILL.md +2 -2
- package/templates/skills/plan/SKILL.md +15 -5
- package/templates/skills/reflect/SKILL.md +1 -1
- package/templates/skills/review/SKILL.md +4 -2
- package/templates/skills/task-work/SKILL.md +16 -6
- package/templates/skills/triage/SKILL.md +1 -1
- package/templates/skills/triage/docs/inbox.md +1 -1
- package/templates/skills/triage/docs/observations.md +1 -1
- package/templates/skills/triage-automation/SKILL.md +2 -1
- package/templates/skills/triage-inbox/SKILL.md +3 -3
- package/templates/skills/writing-specs/SKILL.md +6 -6
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Uses session event storage for full audit trail and streaming output.
|
|
6
6
|
*/
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import { createWriteStream } from "node:fs";
|
|
8
9
|
import * as fs from "node:fs/promises";
|
|
9
10
|
import { createRequire } from "node:module";
|
|
10
11
|
import * as path from "node:path";
|
|
@@ -15,9 +16,10 @@ const require = createRequire(import.meta.url);
|
|
|
15
16
|
const { version: packageVersion } = require("../../../package.json");
|
|
16
17
|
import { registerAdapter, resolveAdapter, } from "../../agents/index.js";
|
|
17
18
|
import { spawnAndInitialize } from "../../agents/spawner.js";
|
|
18
|
-
import { initContext, loadAllItems, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
|
|
19
|
+
import { initContext, loadAllItems, loadMetaContext, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
|
|
20
|
+
import { resolveSkillReferenceTokensForPlatform } from "../../parser/skill-render.js";
|
|
19
21
|
import { buildWrapUpContext, createCliRenderer, createTranslator, DEFAULT_SUBAGENT_PREFIX, DEFAULT_WRAPUP_TIMEOUT, RALPH_PROMPT_TIMEOUT, runSubagent, runWrapUpAgent, WRAPUP_AGENT_PREFIX, } from "../../ralph/index.js";
|
|
20
|
-
import { appendEvent, closeSession, createSessionWithBudget, getSessionBudgetPath, injectEnvForAdapter, isEndLoopRequested, removeEnvForAdapter, requestEndLoop, resetBudget, saveSessionContext, } from "../../sessions/index.js";
|
|
22
|
+
import { appendEvent, closeSession, createSessionWithBudget, getSessionBudgetPath, getSessionDir, injectEnvForAdapter, isEndLoopRequested, removeEnvForAdapter, requestEndLoop, resetBudget, saveSessionContext, } from "../../sessions/index.js";
|
|
21
23
|
import { errors } from "../../strings/index.js";
|
|
22
24
|
import { getCurrentBranch } from "../../utils/git.js";
|
|
23
25
|
import { EXIT_CODES } from "../exit-codes.js";
|
|
@@ -91,7 +93,79 @@ async function allExplicitTasksDone(ctx, scope) {
|
|
|
91
93
|
});
|
|
92
94
|
return { done, statuses };
|
|
93
95
|
}
|
|
94
|
-
|
|
96
|
+
const FALLBACK_CORE_SKILLS = new Set(["task-work", "reflect", "review"]);
|
|
97
|
+
const ADAPTER_VALIDATION_PROBES = [["--help"], ["--version"]];
|
|
98
|
+
const TERMINAL_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
99
|
+
const TOOL_OUTPUT_DIR = "tool-output";
|
|
100
|
+
/**
|
|
101
|
+
* Map adapter IDs to prompt rendering platforms.
|
|
102
|
+
*/
|
|
103
|
+
export function getPromptPlatformForAdapter(adapterId) {
|
|
104
|
+
switch (adapterId) {
|
|
105
|
+
case "claude-agent-acp":
|
|
106
|
+
case "claude-code-acp":
|
|
107
|
+
return "claude-code";
|
|
108
|
+
case "codex-acp":
|
|
109
|
+
return "codex";
|
|
110
|
+
default:
|
|
111
|
+
return "unknown";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build skill origin map from meta skills.
|
|
116
|
+
*/
|
|
117
|
+
async function loadSkillOriginsForRalph(ctx) {
|
|
118
|
+
const meta = await loadMetaContext(ctx);
|
|
119
|
+
const origins = new Map();
|
|
120
|
+
for (const skill of meta.skills) {
|
|
121
|
+
origins.set(skill.id, skill.origin);
|
|
122
|
+
}
|
|
123
|
+
// Fallback for core skills frequently used by ralph, even if core skills
|
|
124
|
+
// were not loaded into project meta for any reason.
|
|
125
|
+
for (const coreSkill of FALLBACK_CORE_SKILLS) {
|
|
126
|
+
if (!origins.has(coreSkill)) {
|
|
127
|
+
origins.set(coreSkill, "core");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return origins;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Normalize legacy literal invocation syntax for a target platform.
|
|
134
|
+
* Keeps backward compatibility for existing slash-style config values.
|
|
135
|
+
*/
|
|
136
|
+
function normalizeLegacyInvocation(invocation, platform) {
|
|
137
|
+
if (platform === "codex") {
|
|
138
|
+
if (/^\/kspec:([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
139
|
+
return invocation.replace(/^\/kspec:([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `$kspec-${skillId}`);
|
|
140
|
+
}
|
|
141
|
+
if (/^\/([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
142
|
+
return invocation.replace(/^\/([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `$${skillId}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (platform === "claude-code") {
|
|
146
|
+
if (/^\$kspec-([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
147
|
+
return invocation.replace(/^\$kspec-([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `/kspec:${skillId}`);
|
|
148
|
+
}
|
|
149
|
+
if (/^\$([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
150
|
+
return invocation.replace(/^\$([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `/${skillId}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return invocation;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve configured skill invocation string for a specific platform.
|
|
157
|
+
* Supports portable {skill:<id>} syntax and legacy literal strings.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveRalphSkillInvocation(invocation, platform, skillOrigins) {
|
|
160
|
+
if (platform === "unknown") {
|
|
161
|
+
return invocation;
|
|
162
|
+
}
|
|
163
|
+
const tokenResolved = resolveSkillReferenceTokensForPlatform(invocation, platform, skillOrigins);
|
|
164
|
+
if (tokenResolved !== invocation) {
|
|
165
|
+
return tokenResolved;
|
|
166
|
+
}
|
|
167
|
+
return normalizeLegacyInvocation(invocation, platform);
|
|
168
|
+
}
|
|
95
169
|
// AC: @ralph-skill-delegation ac-1, ac-2, ac-3
|
|
96
170
|
function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, skillTaskWork, focus, explicitTaskScope) {
|
|
97
171
|
const focusSection = focus
|
|
@@ -174,46 +248,182 @@ ${isFinal
|
|
|
174
248
|
Exit when reflection is complete.
|
|
175
249
|
`;
|
|
176
250
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Check whether an adapter package appears to be installed and executable.
|
|
253
|
+
* Uses multiple non-installing probes because CLIs differ on supported flags.
|
|
254
|
+
*/
|
|
255
|
+
export function isAdapterPackageAvailable(adapterPackage, runner = spawnSync) {
|
|
256
|
+
for (const probeArgs of ADAPTER_VALIDATION_PROBES) {
|
|
257
|
+
const result = runner("npx", ["--no-install", adapterPackage, ...probeArgs], {
|
|
258
|
+
encoding: "utf-8",
|
|
259
|
+
stdio: "pipe",
|
|
260
|
+
});
|
|
261
|
+
if (result.status === 0) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
182
267
|
/**
|
|
183
268
|
* Validate that the specified ACP adapter package exists.
|
|
184
|
-
* Uses npx --no-install to check both global and local node_modules.
|
|
269
|
+
* Uses npx --no-install probes to check both global and local node_modules.
|
|
185
270
|
*
|
|
186
271
|
* @throws {Error} Never throws - exits process with code 3 if validation fails
|
|
187
272
|
*/
|
|
188
273
|
function validateAdapter(adapterPackage) {
|
|
189
|
-
|
|
190
|
-
// This checks both global and local node_modules, handles scoped packages
|
|
191
|
-
const result = spawnSync("npx", ["--no-install", adapterPackage, "--version"], {
|
|
192
|
-
encoding: "utf-8",
|
|
193
|
-
stdio: "pipe",
|
|
194
|
-
});
|
|
195
|
-
if (result.status !== 0) {
|
|
274
|
+
if (!isAdapterPackageAvailable(adapterPackage)) {
|
|
196
275
|
error(`Adapter package not found: ${adapterPackage}. Install with: npm install -g ${adapterPackage}`);
|
|
197
276
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
198
277
|
}
|
|
199
278
|
}
|
|
279
|
+
function sanitizeToolCallId(toolCallId) {
|
|
280
|
+
const raw = String(toolCallId).trim();
|
|
281
|
+
if (!raw) {
|
|
282
|
+
return "tool-call";
|
|
283
|
+
}
|
|
284
|
+
return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
285
|
+
}
|
|
286
|
+
function updateStreamPreview(state, chunk, maxPreviewBytes) {
|
|
287
|
+
state.bytes += chunk.length;
|
|
288
|
+
const remaining = maxPreviewBytes - state.previewBytes;
|
|
289
|
+
if (remaining <= 0) {
|
|
290
|
+
state.truncated = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (chunk.length > remaining) {
|
|
294
|
+
state.previewParts.push(chunk.subarray(0, remaining).toString("utf-8"));
|
|
295
|
+
state.previewBytes += remaining;
|
|
296
|
+
state.truncated = true;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
state.previewParts.push(chunk.toString("utf-8"));
|
|
300
|
+
state.previewBytes += chunk.length;
|
|
301
|
+
}
|
|
302
|
+
function closeStream(stream) {
|
|
303
|
+
if (!stream) {
|
|
304
|
+
return Promise.resolve();
|
|
305
|
+
}
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
const onError = (err) => {
|
|
308
|
+
stream.off("finish", onFinish);
|
|
309
|
+
reject(err);
|
|
310
|
+
};
|
|
311
|
+
const onFinish = () => {
|
|
312
|
+
stream.off("error", onError);
|
|
313
|
+
resolve();
|
|
314
|
+
};
|
|
315
|
+
stream.once("error", onError);
|
|
316
|
+
stream.once("finish", onFinish);
|
|
317
|
+
stream.end();
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Execute terminal/run request with bounded in-memory preview and streamed
|
|
322
|
+
* session artifacts for full stdout/stderr retention.
|
|
323
|
+
*/
|
|
324
|
+
export async function runTerminalCommandWithArtifacts(options) {
|
|
325
|
+
const previewMaxBytes = options.previewMaxBytes ?? TERMINAL_PREVIEW_MAX_BYTES;
|
|
326
|
+
const shouldWriteArtifacts = Boolean(options.specDir && options.sessionId);
|
|
327
|
+
let stdoutPath;
|
|
328
|
+
let stderrPath;
|
|
329
|
+
if (shouldWriteArtifacts) {
|
|
330
|
+
const outputDir = path.join(getSessionDir(options.specDir, options.sessionId), TOOL_OUTPUT_DIR);
|
|
331
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
332
|
+
const safeToolCallId = sanitizeToolCallId(options.toolCallId);
|
|
333
|
+
stdoutPath = path.join(outputDir, `${safeToolCallId}.stdout.log`);
|
|
334
|
+
stderrPath = path.join(outputDir, `${safeToolCallId}.stderr.log`);
|
|
335
|
+
}
|
|
336
|
+
const stdoutState = {
|
|
337
|
+
bytes: 0,
|
|
338
|
+
previewBytes: 0,
|
|
339
|
+
previewParts: [],
|
|
340
|
+
truncated: false,
|
|
341
|
+
stream: stdoutPath ? createWriteStream(stdoutPath) : undefined,
|
|
342
|
+
};
|
|
343
|
+
const stderrState = {
|
|
344
|
+
bytes: 0,
|
|
345
|
+
previewBytes: 0,
|
|
346
|
+
previewParts: [],
|
|
347
|
+
truncated: false,
|
|
348
|
+
stream: stderrPath ? createWriteStream(stderrPath) : undefined,
|
|
349
|
+
};
|
|
350
|
+
return await new Promise((resolve, reject) => {
|
|
351
|
+
let settled = false;
|
|
352
|
+
const child = spawn(options.command, [], {
|
|
353
|
+
cwd: options.cwd,
|
|
354
|
+
shell: true,
|
|
355
|
+
timeout: options.timeout,
|
|
356
|
+
});
|
|
357
|
+
const finalize = async (exitCode, errorMessage) => {
|
|
358
|
+
if (settled) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
settled = true;
|
|
362
|
+
if (errorMessage) {
|
|
363
|
+
const errChunk = Buffer.from(errorMessage, "utf-8");
|
|
364
|
+
stderrState.stream?.write(errChunk);
|
|
365
|
+
updateStreamPreview(stderrState, errChunk, previewMaxBytes);
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
await Promise.all([
|
|
369
|
+
closeStream(stdoutState.stream),
|
|
370
|
+
closeStream(stderrState.stream),
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
catch (streamErr) {
|
|
374
|
+
reject(streamErr);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
resolve({
|
|
378
|
+
stdout: stdoutState.previewParts.join(""),
|
|
379
|
+
stderr: stderrState.previewParts.join(""),
|
|
380
|
+
exitCode,
|
|
381
|
+
stdout_path: stdoutPath,
|
|
382
|
+
stderr_path: stderrPath,
|
|
383
|
+
stdout_bytes: stdoutState.bytes,
|
|
384
|
+
stderr_bytes: stderrState.bytes,
|
|
385
|
+
preview_truncated: stdoutState.truncated || stderrState.truncated,
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
child.stdout?.on("data", (data) => {
|
|
389
|
+
const chunk = Buffer.isBuffer(data)
|
|
390
|
+
? data
|
|
391
|
+
: Buffer.from(String(data), "utf-8");
|
|
392
|
+
stdoutState.stream?.write(chunk);
|
|
393
|
+
updateStreamPreview(stdoutState, chunk, previewMaxBytes);
|
|
394
|
+
});
|
|
395
|
+
child.stderr?.on("data", (data) => {
|
|
396
|
+
const chunk = Buffer.isBuffer(data)
|
|
397
|
+
? data
|
|
398
|
+
: Buffer.from(String(data), "utf-8");
|
|
399
|
+
stderrState.stream?.write(chunk);
|
|
400
|
+
updateStreamPreview(stderrState, chunk, previewMaxBytes);
|
|
401
|
+
});
|
|
402
|
+
child.on("close", (code) => {
|
|
403
|
+
void finalize(code ?? 1);
|
|
404
|
+
});
|
|
405
|
+
child.on("error", (err) => {
|
|
406
|
+
void finalize(1, err.message);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
200
410
|
// ─── Tool Request Handler ────────────────────────────────────────────────────
|
|
201
411
|
/**
|
|
202
412
|
* Handle tool requests from ACP agent.
|
|
203
413
|
* Implements file operations, terminal commands, and permission handling.
|
|
204
414
|
*/
|
|
205
|
-
async function handleRequest(client, id, method, params,
|
|
415
|
+
async function handleRequest(client, id, method, params, options) {
|
|
206
416
|
try {
|
|
207
417
|
switch (method) {
|
|
208
418
|
case "session/request_permission": {
|
|
209
419
|
const p = params;
|
|
210
420
|
// In yolo mode, auto-approve all permissions
|
|
211
421
|
// In normal mode, would need to implement permission UI
|
|
212
|
-
const
|
|
213
|
-
if (yolo) {
|
|
422
|
+
const permissionOptions = p.options || [];
|
|
423
|
+
if (options.yolo) {
|
|
214
424
|
// Find an "allow" option (prefer allow_always, then allow_once)
|
|
215
|
-
const allowOption =
|
|
216
|
-
|
|
425
|
+
const allowOption = permissionOptions.find((o) => o.kind === "allow_always") ||
|
|
426
|
+
permissionOptions.find((o) => o.kind === "allow_once");
|
|
217
427
|
if (allowOption) {
|
|
218
428
|
client.respondPermission(id, {
|
|
219
429
|
outcome: { outcome: "selected", optionId: allowOption.optionId },
|
|
@@ -250,26 +460,13 @@ async function handleRequest(client, id, method, params, yolo) {
|
|
|
250
460
|
const command = p.command;
|
|
251
461
|
const cwd = p.cwd || process.cwd();
|
|
252
462
|
const timeout = p.timeout || 60000;
|
|
253
|
-
const result = await
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
let stderr = "";
|
|
261
|
-
child.stdout?.on("data", (data) => {
|
|
262
|
-
stdout += data.toString();
|
|
263
|
-
});
|
|
264
|
-
child.stderr?.on("data", (data) => {
|
|
265
|
-
stderr += data.toString();
|
|
266
|
-
});
|
|
267
|
-
child.on("close", (code) => {
|
|
268
|
-
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
269
|
-
});
|
|
270
|
-
child.on("error", (err) => {
|
|
271
|
-
resolve({ stdout, stderr: err.message, exitCode: 1 });
|
|
272
|
-
});
|
|
463
|
+
const result = await runTerminalCommandWithArtifacts({
|
|
464
|
+
command,
|
|
465
|
+
cwd,
|
|
466
|
+
timeout,
|
|
467
|
+
toolCallId: id,
|
|
468
|
+
specDir: options.specDir,
|
|
469
|
+
sessionId: options.sessionId,
|
|
273
470
|
});
|
|
274
471
|
// Using generic respond() since this is a custom method
|
|
275
472
|
client.respond(id, result);
|
|
@@ -488,11 +685,16 @@ async function processPendingReviewTasks(ctx, adapter, pendingReviewTasks, optio
|
|
|
488
685
|
const result = await runSubagent(adapter, subagentCtx, {
|
|
489
686
|
timeout: options.subagentTimeout,
|
|
490
687
|
outputPrefix: DEFAULT_SUBAGENT_PREFIX,
|
|
491
|
-
skillName:
|
|
688
|
+
skillName: options.prReviewSkillName,
|
|
492
689
|
}, {
|
|
493
690
|
yolo: options.yolo,
|
|
494
691
|
cwd: options.cwd,
|
|
495
|
-
|
|
692
|
+
extraArgs: options.autoApproveArgs,
|
|
693
|
+
handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, {
|
|
694
|
+
yolo: options.yolo,
|
|
695
|
+
specDir: options.specDir,
|
|
696
|
+
sessionId: options.sessionId,
|
|
697
|
+
}),
|
|
496
698
|
});
|
|
497
699
|
if (result.timedOut) {
|
|
498
700
|
// AC: @ralph-subagent-spawning ac-9
|
|
@@ -612,6 +814,8 @@ export function registerRalphCommand(program) {
|
|
|
612
814
|
.option("--no-yolo", "Require normal permission prompts")
|
|
613
815
|
.option("--subagent-timeout <minutes>", "Review subagent timeout in minutes", "20")
|
|
614
816
|
.option("--adapter <id>", "Agent adapter to use", "claude-agent-acp")
|
|
817
|
+
.option("--worker-adapter <id>", "Adapter for task-work agent (overrides --adapter)")
|
|
818
|
+
.option("--reviewer-adapter <id>", "Adapter for review subagent (overrides --adapter)")
|
|
615
819
|
.option("--adapter-cmd <cmd>", "Custom adapter command (for testing)")
|
|
616
820
|
.option("--restart-every <n>", "Restart agent every N iterations to prevent OOM (0 = never)", "10")
|
|
617
821
|
.option("--focus <instructions>", "Focus instructions included in every iteration prompt")
|
|
@@ -679,28 +883,32 @@ export function registerRalphCommand(program) {
|
|
|
679
883
|
registerAdapter("custom", customAdapter);
|
|
680
884
|
options.adapter = "custom";
|
|
681
885
|
}
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// -
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
// Add yolo flag to adapter args if needed (accept both new and deprecated names)
|
|
701
|
-
if (options.yolo && isDefaultAdapter) {
|
|
702
|
-
adapter.args = [...adapter.args, "--dangerously-skip-permissions"];
|
|
886
|
+
// AC: @ralph-per-role-adapters ac-3, ac-4, ac-5
|
|
887
|
+
// Resolve per-role adapters with precedence: role flag > --adapter > default
|
|
888
|
+
const workerAdapterId = options.workerAdapter ?? options.adapter;
|
|
889
|
+
const reviewerAdapterId = options.reviewerAdapter ?? options.adapter;
|
|
890
|
+
const workerAdapter = resolveAdapter(workerAdapterId);
|
|
891
|
+
const reviewerAdapter = resolveAdapter(reviewerAdapterId);
|
|
892
|
+
// AC: @ralph-per-role-adapters ac-6, ac-9, ac-11
|
|
893
|
+
// Validate adapter packages — deduplicate when same ID
|
|
894
|
+
const adapterIdsToValidate = new Set([workerAdapterId, reviewerAdapterId]);
|
|
895
|
+
for (const id of adapterIdsToValidate) {
|
|
896
|
+
const resolved = resolveAdapter(id);
|
|
897
|
+
const isDefault = id === "claude-agent-acp" || id === "claude-code-acp";
|
|
898
|
+
const skip = resolved.command !== "npx" ||
|
|
899
|
+
!resolved.args[0] ||
|
|
900
|
+
(options.dryRun && isDefault);
|
|
901
|
+
if (!skip) {
|
|
902
|
+
validateAdapter(resolved.args[0]);
|
|
903
|
+
}
|
|
703
904
|
}
|
|
905
|
+
// Build auto-approve extra args per adapter (applied per-spawn to prevent cross-role leakage)
|
|
906
|
+
const workerAutoApproveArgs = options.yolo
|
|
907
|
+
? workerAdapter.autoApproveArgs
|
|
908
|
+
: undefined;
|
|
909
|
+
const reviewerAutoApproveArgs = options.yolo
|
|
910
|
+
? reviewerAdapter.autoApproveArgs
|
|
911
|
+
: undefined;
|
|
704
912
|
const restartInfo = restartEvery > 0 ? `, restart every ${restartEvery}` : "";
|
|
705
913
|
const maxTasksInfo = maxTasks === 0 ? "unlimited" : `${maxTasks}`;
|
|
706
914
|
// Initialize kspec context early to validate --tasks
|
|
@@ -717,10 +925,19 @@ export function registerRalphCommand(program) {
|
|
|
717
925
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
718
926
|
}
|
|
719
927
|
}
|
|
928
|
+
const skillOrigins = await loadSkillOriginsForRalph(ctx);
|
|
929
|
+
const workerPromptPlatform = getPromptPlatformForAdapter(workerAdapterId);
|
|
930
|
+
const reviewerPromptPlatform = getPromptPlatformForAdapter(reviewerAdapterId);
|
|
931
|
+
const workerTaskWorkSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.task_work, workerPromptPlatform, skillOrigins);
|
|
932
|
+
const workerReflectSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.reflect, workerPromptPlatform, skillOrigins);
|
|
933
|
+
const reviewerPrReviewSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.pr_review, reviewerPromptPlatform, skillOrigins);
|
|
720
934
|
const taskScopeInfo = explicitTaskScope
|
|
721
935
|
? `, tasks=${explicitTaskScope.refs.join(",")}`
|
|
722
936
|
: "";
|
|
723
|
-
|
|
937
|
+
const adapterInfo = workerAdapterId === reviewerAdapterId
|
|
938
|
+
? `adapter=${workerAdapterId}`
|
|
939
|
+
: `worker=${workerAdapterId}, reviewer=${reviewerAdapterId}`;
|
|
940
|
+
info(`Starting ralph loop (${adapterInfo}, max ${maxLoops} iterations, ${maxRetries} retries, ${maxFailures} max failures${restartInfo}, max-tasks=${maxTasksInfo}${taskScopeInfo})`);
|
|
724
941
|
if (options.focus) {
|
|
725
942
|
info(`Focus: ${options.focus}`);
|
|
726
943
|
}
|
|
@@ -738,12 +955,20 @@ export function registerRalphCommand(program) {
|
|
|
738
955
|
// Create session with budget. When maxTasks=0 (unlimited), no budget.json is created.
|
|
739
956
|
await createSessionWithBudget(specDir, {
|
|
740
957
|
id: sessionId,
|
|
741
|
-
agent_type:
|
|
958
|
+
agent_type: workerAdapterId,
|
|
742
959
|
budget: maxTasks,
|
|
743
960
|
});
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
961
|
+
// AC: @ralph-per-role-adapters ac-6, ac-7
|
|
962
|
+
// Adapter IDs for harness-specific env injection/cleanup.
|
|
963
|
+
// Deduplicate by harness target, not just adapter ID. claude-code-acp is
|
|
964
|
+
// an alias for claude-agent-acp — both inject to the same Claude Code
|
|
965
|
+
// settings file. Without normalization, injecting twice would clobber the
|
|
966
|
+
// previousValue and break cleanup restoration.
|
|
967
|
+
const normalizeForEnv = (id) => id === "claude-code-acp" ? "claude-agent-acp" : id;
|
|
968
|
+
const uniqueAdapterIds = [...new Set([
|
|
969
|
+
normalizeForEnv(workerAdapterId),
|
|
970
|
+
normalizeForEnv(reviewerAdapterId),
|
|
971
|
+
])];
|
|
747
972
|
// Everything after session creation is wrapped in try/finally to guarantee
|
|
748
973
|
// budget cleanup even if pre-loop setup (event logging, signal handlers) throws.
|
|
749
974
|
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
@@ -753,7 +978,9 @@ export function registerRalphCommand(program) {
|
|
|
753
978
|
let exitReason = null;
|
|
754
979
|
let lastIterationCtx = null;
|
|
755
980
|
let lastErrorMessage;
|
|
756
|
-
|
|
981
|
+
// AC: @ralph-per-role-adapters ac-7
|
|
982
|
+
// Track previous env values per adapter for cleanup restoration
|
|
983
|
+
const previousEnvValues = new Map();
|
|
757
984
|
const recentTaskRefs = [];
|
|
758
985
|
const sessionIterationMap = new Map();
|
|
759
986
|
// Signal handler refs — declared here so finally can remove them
|
|
@@ -772,7 +999,7 @@ export function registerRalphCommand(program) {
|
|
|
772
999
|
await Promise.all([
|
|
773
1000
|
fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { }),
|
|
774
1001
|
closeSession(specDir, sessionId, "abandoned", `Received ${signal}`),
|
|
775
|
-
removeEnvForAdapter(
|
|
1002
|
+
...uniqueAdapterIds.map((id) => removeEnvForAdapter(id, previousEnvValues.get(id))),
|
|
776
1003
|
]);
|
|
777
1004
|
}
|
|
778
1005
|
catch {
|
|
@@ -792,19 +1019,24 @@ export function registerRalphCommand(program) {
|
|
|
792
1019
|
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
793
1020
|
process.on("SIGINT", sigintHandler);
|
|
794
1021
|
process.on("SIGTERM", sigtermHandler);
|
|
795
|
-
//
|
|
796
|
-
//
|
|
1022
|
+
// AC: @ralph-per-role-adapters ac-6, ac-7
|
|
1023
|
+
// Inject KSPEC_SESSION_ID into agent harness config for each unique adapter.
|
|
797
1024
|
// Process env alone is insufficient — some harnesses (e.g., Claude Code)
|
|
798
1025
|
// sandbox child processes and don't forward arbitrary parent env vars.
|
|
799
1026
|
// AC: @ralph-session-budget-integration ac-env-inject
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
1027
|
+
for (const id of uniqueAdapterIds) {
|
|
1028
|
+
const injectionResult = await injectEnvForAdapter(id, sessionId);
|
|
1029
|
+
previousEnvValues.set(id, injectionResult?.previousValue);
|
|
1030
|
+
}
|
|
1031
|
+
// AC: @ralph-per-role-adapters ac-12
|
|
1032
|
+
// Log session start with both adapter IDs
|
|
803
1033
|
await appendEvent(specDir, {
|
|
804
1034
|
session_id: sessionId,
|
|
805
1035
|
type: "session.start",
|
|
806
1036
|
data: {
|
|
807
|
-
adapter:
|
|
1037
|
+
adapter: workerAdapterId,
|
|
1038
|
+
workerAdapter: workerAdapterId,
|
|
1039
|
+
reviewerAdapter: reviewerAdapterId,
|
|
808
1040
|
maxLoops,
|
|
809
1041
|
maxRetries,
|
|
810
1042
|
maxFailures,
|
|
@@ -841,14 +1073,19 @@ export function registerRalphCommand(program) {
|
|
|
841
1073
|
sessionCtx = filterByExplicitTasks(sessionCtx, explicitTaskScope);
|
|
842
1074
|
}
|
|
843
1075
|
// AC: @ralph-subagent-spawning ac-8 - Process pending_review tasks BEFORE main iteration
|
|
1076
|
+
// AC: @ralph-per-role-adapters ac-2 - Use reviewer adapter for review subagents
|
|
844
1077
|
// This wraps consecutiveFailures in an object so it can be mutated by the helper
|
|
845
1078
|
const failureTracker = { count: consecutiveFailures };
|
|
846
|
-
const continueLoop = await processPendingReviewTasks(ctx,
|
|
1079
|
+
const continueLoop = await processPendingReviewTasks(ctx, reviewerAdapter, sessionCtx.pending_review_tasks, {
|
|
847
1080
|
yolo: options.yolo,
|
|
848
1081
|
maxRetries,
|
|
849
1082
|
maxFailures,
|
|
850
1083
|
cwd: process.cwd(),
|
|
1084
|
+
specDir,
|
|
1085
|
+
sessionId,
|
|
851
1086
|
subagentTimeout: subagentTimeout * 60 * 1000,
|
|
1087
|
+
autoApproveArgs: reviewerAutoApproveArgs,
|
|
1088
|
+
prReviewSkillName: reviewerPrReviewSkill,
|
|
852
1089
|
}, failureTracker);
|
|
853
1090
|
consecutiveFailures = failureTracker.count;
|
|
854
1091
|
if (!continueLoop) {
|
|
@@ -902,16 +1139,22 @@ export function registerRalphCommand(program) {
|
|
|
902
1139
|
const iterationStartTime = new Date();
|
|
903
1140
|
// Build prompts - task-work first, then reflect
|
|
904
1141
|
// AC: @cli-ralph ac-21 - Include explicit task scope in prompt
|
|
905
|
-
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId,
|
|
906
|
-
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId,
|
|
1142
|
+
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, workerTaskWorkSkill, options.focus, explicitTaskScope);
|
|
1143
|
+
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId, workerReflectSkill);
|
|
907
1144
|
// AC: @cli-ralph ac-21
|
|
1145
|
+
// AC: @ralph-per-role-adapters ac-10
|
|
908
1146
|
if (options.dryRun) {
|
|
909
1147
|
console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
|
|
1148
|
+
console.log(` worker-adapter: ${workerAdapterId}`);
|
|
1149
|
+
console.log(` reviewer-adapter: ${reviewerAdapterId}`);
|
|
910
1150
|
console.log(` max-loops: ${maxLoops}`);
|
|
911
1151
|
console.log(` max-tasks: ${maxTasks === 0 ? "unlimited" : maxTasks}`);
|
|
912
1152
|
console.log(` max-retries: ${maxRetries}`);
|
|
913
1153
|
console.log(` max-failures: ${maxFailures}`);
|
|
914
1154
|
console.log(` restart-every: ${restartEvery === 0 ? "never" : restartEvery}`);
|
|
1155
|
+
console.log(` worker-task-work-skill: ${workerTaskWorkSkill}`);
|
|
1156
|
+
console.log(` worker-reflect-skill: ${workerReflectSkill}`);
|
|
1157
|
+
console.log(` reviewer-pr-review-skill: ${reviewerPrReviewSkill}`);
|
|
915
1158
|
if (explicitTaskScope) {
|
|
916
1159
|
console.log(` explicit-tasks: ${explicitTaskScope.refs.join(", ")}`);
|
|
917
1160
|
}
|
|
@@ -945,12 +1188,15 @@ export function registerRalphCommand(program) {
|
|
|
945
1188
|
}
|
|
946
1189
|
try {
|
|
947
1190
|
// Spawn agent if not already running
|
|
1191
|
+
// AC: @ralph-per-role-adapters ac-1 - Use worker adapter for task-work
|
|
948
1192
|
if (!agent) {
|
|
949
1193
|
info("Spawning ACP agent...");
|
|
950
1194
|
// AC: @ralph-session-budget-integration ac-env-inject
|
|
951
|
-
|
|
1195
|
+
// AC: @ralph-adapter-auto-approve ac-1, ac-2, ac-3
|
|
1196
|
+
agent = await spawnAndInitialize(workerAdapter, {
|
|
952
1197
|
cwd: process.cwd(),
|
|
953
1198
|
env: { KSPEC_SESSION_ID: sessionId },
|
|
1199
|
+
extraArgs: workerAutoApproveArgs,
|
|
954
1200
|
clientOptions: {
|
|
955
1201
|
clientInfo: {
|
|
956
1202
|
name: "kspec-ralph",
|
|
@@ -984,7 +1230,11 @@ export function registerRalphCommand(program) {
|
|
|
984
1230
|
// Set up tool request handler
|
|
985
1231
|
agent.client.on("request", (reqId, method, params) => {
|
|
986
1232
|
// biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
|
|
987
|
-
handleRequest(agent.client, reqId, method, params,
|
|
1233
|
+
handleRequest(agent.client, reqId, method, params, {
|
|
1234
|
+
yolo: options.yolo,
|
|
1235
|
+
specDir,
|
|
1236
|
+
sessionId,
|
|
1237
|
+
}).catch((err) => {
|
|
988
1238
|
// biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
|
|
989
1239
|
agent.client.respondError(reqId, -32000, err.message);
|
|
990
1240
|
});
|
|
@@ -1127,9 +1377,11 @@ export function registerRalphCommand(program) {
|
|
|
1127
1377
|
agent = null;
|
|
1128
1378
|
}
|
|
1129
1379
|
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1130
|
-
//
|
|
1380
|
+
// AC: @ralph-per-role-adapters ac-7 - Clean up env for all unique adapters
|
|
1131
1381
|
await fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { });
|
|
1132
|
-
|
|
1382
|
+
for (const id of uniqueAdapterIds) {
|
|
1383
|
+
await removeEnvForAdapter(id, previousEnvValues.get(id));
|
|
1384
|
+
}
|
|
1133
1385
|
// Clean up session env vars
|
|
1134
1386
|
delete process.env.KSPEC_RALPH_SESSION;
|
|
1135
1387
|
delete process.env.KSPEC_SESSION_ID;
|
|
@@ -1147,10 +1399,16 @@ export function registerRalphCommand(program) {
|
|
|
1147
1399
|
maxLoops, inProgressTasks, pendingReviewTasks, recentTaskRefs, process.cwd(), lastErrorMessage);
|
|
1148
1400
|
info(`Exit reason: ${exitReason}`);
|
|
1149
1401
|
info(`Working tree: ${wrapUpCtx.workingTree.clean ? "clean" : "has uncommitted changes"}`);
|
|
1150
|
-
|
|
1402
|
+
// AC: @ralph-per-role-adapters ac-8 - Wrap-up uses worker adapter
|
|
1403
|
+
const wrapUpResult = await runWrapUpAgent(workerAdapter, wrapUpCtx, {
|
|
1151
1404
|
yolo: options.yolo,
|
|
1152
1405
|
cwd: process.cwd(),
|
|
1153
|
-
|
|
1406
|
+
extraArgs: workerAutoApproveArgs,
|
|
1407
|
+
handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, {
|
|
1408
|
+
yolo: options.yolo,
|
|
1409
|
+
specDir,
|
|
1410
|
+
sessionId,
|
|
1411
|
+
}),
|
|
1154
1412
|
}, DEFAULT_WRAPUP_TIMEOUT);
|
|
1155
1413
|
// Log wrap-up result
|
|
1156
1414
|
await appendEvent(specDir, {
|