@kynetic-ai/spec 0.9.1 → 0.11.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/README.md +2 -1
- package/dist/acp/client.d.ts +6 -1
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +7 -2
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +12 -1
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +27 -4
- package/dist/acp/framing.js.map +1 -1
- package/dist/agent-runtime/dispatch.d.ts +292 -0
- package/dist/agent-runtime/dispatch.d.ts.map +1 -0
- package/dist/agent-runtime/dispatch.js +860 -0
- package/dist/agent-runtime/dispatch.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +11 -0
- package/dist/agent-runtime/index.d.ts.map +1 -0
- package/dist/agent-runtime/index.js +11 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/invocation.d.ts +86 -0
- package/dist/agent-runtime/invocation.d.ts.map +1 -0
- package/dist/agent-runtime/invocation.js +442 -0
- package/dist/agent-runtime/invocation.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +50 -0
- package/dist/agent-runtime/prompts.d.ts.map +1 -0
- package/dist/agent-runtime/prompts.js +108 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +60 -4
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-exec.d.ts.map +1 -1
- package/dist/cli/batch-exec.js +140 -62
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/batch-write-buffer.d.ts +141 -0
- package/dist/cli/batch-write-buffer.d.ts.map +1 -0
- package/dist/cli/batch-write-buffer.js +400 -0
- package/dist/cli/batch-write-buffer.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +20 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +831 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +46 -22
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +22 -16
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.js +1 -1
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +168 -6
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +2 -1
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.js +19 -3
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +87 -43
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts +5 -56
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +52 -1502
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +22 -13
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +70 -11
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
- package/dist/cli/commands/session/checkpoint.js +7 -2
- package/dist/cli/commands/session/checkpoint.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +15 -0
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/context.d.ts.map +1 -1
- package/dist/cli/commands/session/context.js +10 -5
- package/dist/cli/commands/session/context.js.map +1 -1
- package/dist/cli/commands/session/log.d.ts +1 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +124 -8
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/session/stale-close.d.ts +17 -0
- package/dist/cli/commands/session/stale-close.d.ts.map +1 -0
- package/dist/cli/commands/session/stale-close.js +378 -0
- package/dist/cli/commands/session/stale-close.js.map +1 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +95 -0
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts.map +1 -1
- package/dist/cli/commands/skill-crud.js +4 -3
- package/dist/cli/commands/skill-crud.js.map +1 -1
- package/dist/cli/commands/skill-diff.d.ts.map +1 -1
- package/dist/cli/commands/skill-diff.js +15 -0
- package/dist/cli/commands/skill-diff.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +50 -18
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +536 -310
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.js +1 -1
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +37 -13
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +65 -25
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +5 -0
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +5 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/daemon/project-context.ts +22 -0
- package/dist/daemon/routes/agent-dispatch.ts +279 -0
- package/dist/daemon/routes/items.ts +22 -0
- package/dist/daemon/routes/meta.ts +141 -1
- package/dist/daemon/routes/plans.ts +147 -0
- package/dist/daemon/routes/sessions.ts +180 -0
- package/dist/daemon/routes/tasks.ts +198 -0
- package/dist/daemon/routes/validation.ts +1 -1
- package/dist/daemon/server.ts +77 -21
- package/dist/daemon/websocket/handler.ts +67 -6
- package/dist/daemon/websocket/lifecycle.ts +19 -0
- package/dist/daemon/websocket/pubsub.ts +74 -3
- package/dist/export/html.d.ts.map +1 -1
- package/dist/export/html.js +5 -2
- package/dist/export/html.js.map +1 -1
- package/dist/export/triage.d.ts +1 -1
- package/dist/export/triage.d.ts.map +1 -1
- package/dist/export/triage.js +5 -3
- package/dist/export/triage.js.map +1 -1
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +10 -5
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.js +1 -1
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +6 -6
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +9 -8
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +12 -12
- package/dist/parser/plans.d.ts +7 -0
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +100 -15
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/refs.d.ts +5 -0
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +17 -12
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/shadow.d.ts +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +71 -4
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +6 -3
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +35 -76
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +24 -5
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +224 -64
- package/dist/parser/yaml.js.map +1 -1
- package/dist/schema/meta.d.ts +457 -119
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +56 -0
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +22 -22
- package/dist/schema/spec.d.ts +39 -39
- package/dist/schema/task.d.ts +43 -32
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +5 -0
- package/dist/schema/task.js.map +1 -1
- package/dist/sessions/store.d.ts +126 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +440 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +75 -17
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +51 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/triage/actions.d.ts +1 -0
- package/dist/triage/actions.d.ts.map +1 -1
- package/dist/triage/actions.js +34 -7
- package/dist/triage/actions.js.map +1 -1
- package/dist/utils/commit.js +1 -1
- package/dist/utils/commit.js.map +1 -1
- package/dist/web-ui/_app/env.js +1 -0
- package/dist/web-ui/_app/immutable/assets/0.BJaYkGW2.css +1 -0
- package/dist/web-ui/_app/immutable/assets/9.SzGLxi4x.css +1 -0
- package/dist/web-ui/_app/immutable/assets/select-trigger.CV-KWLNP.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/-lc0BifF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/62JVKtnb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/8RBjHMN1.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5LJFxqa.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5wTVqxm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B6VSmczZ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B8a0xDxR.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BEOQc37C.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BHtYorjv.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BJ0JX3ea.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BMuCqDX8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BP352uRn.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BUZujXJ2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BVA9Exy-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BWET-efb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BXkNecpt.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BYzrIfX8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BkOJ8DkV.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BpuwufMc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BwMO4RrG.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BysXJlZb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C076q4JN.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C33JaVbg.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CGtqifKp.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CHDZZ7OG.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CPPfDSei.js +5 -0
- package/dist/web-ui/_app/immutable/chunks/CUir3f4J.js +60 -0
- package/dist/web-ui/_app/immutable/chunks/Cncwi6fQ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CrCIbn0C.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CwELQvbx.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D3vxvonu.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D6TVmR9T.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D7LTux4W.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D82RulSH.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D9QNBZM2.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DAMmvwn4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DAh4Wfku.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DAx07bEQ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DBYE9jOd.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DOno4cA2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DQA8NZIH.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DRfPm2bo.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DhQhksaB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DjG7s6hm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DjcCz-PU.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DkltRNvh.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DlaTnPKL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DvA-KON-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DxCk-KHc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzO4hlg9.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Eo4gF7ih.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/ExCq5swK.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/T3zZGv51.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/XZumBYeP.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/_ySfNjkF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/iEtR5cV6.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/k_Qegko0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/pE6cYWlS.js +1 -0
- package/dist/web-ui/_app/immutable/entry/app.Cgu6uKeS.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.9XifnLoB.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.DISwcKSK.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.Cx2Ufqp1.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/10.C3z8ijXL.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/11.DZdIjZmM.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/12.FsIGfAOa.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/13.DZoFwagf.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/14.DaIzDKbQ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/15.BYyt4XWF.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/16.CQkSqpOe.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.Bkf_j2UJ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.kaMCurJG.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.BSsFPTHG.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/5.CpPlcCEZ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.BN4FqQmY.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.9kBYIZik.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/8.BuijtZ6B.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/9.C-Weba8R.js +1 -0
- package/dist/web-ui/_app/version.json +1 -0
- package/dist/web-ui/index.html +39 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +4 -2
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +25 -2
- package/templates/agents-sections/06-ralph-loop.md +64 -11
- package/templates/skills/task-work/SKILL.md +25 -2
- package/dist/ralph/cli-renderer.d.ts +0 -27
- package/dist/ralph/cli-renderer.d.ts.map +0 -1
- package/dist/ralph/cli-renderer.js +0 -250
- package/dist/ralph/cli-renderer.js.map +0 -1
- package/dist/ralph/events.d.ts +0 -65
- package/dist/ralph/events.d.ts.map +0 -1
- package/dist/ralph/events.js +0 -600
- package/dist/ralph/events.js.map +0 -1
- package/dist/ralph/index.d.ts +0 -11
- package/dist/ralph/index.d.ts.map +0 -1
- package/dist/ralph/index.js +0 -16
- package/dist/ralph/index.js.map +0 -1
- package/dist/ralph/loop-errors.d.ts +0 -83
- package/dist/ralph/loop-errors.d.ts.map +0 -1
- package/dist/ralph/loop-errors.js +0 -150
- package/dist/ralph/loop-errors.js.map +0 -1
- package/dist/ralph/subagent.d.ts +0 -127
- package/dist/ralph/subagent.d.ts.map +0 -1
- package/dist/ralph/subagent.js +0 -268
- package/dist/ralph/subagent.js.map +0 -1
- package/dist/ralph/wrap-up.d.ts +0 -127
- package/dist/ralph/wrap-up.d.ts.map +0 -1
- package/dist/ralph/wrap-up.js +0 -271
- package/dist/ralph/wrap-up.js.map +0 -1
|
@@ -1,1521 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ralph command -
|
|
2
|
+
* Ralph command - deprecated.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Ralph has been replaced by `kspec agent`. This stub provides a helpful
|
|
5
|
+
* migration error message when users run `kspec ralph`.
|
|
6
|
+
*
|
|
7
|
+
* AC: @ralph-replacement ac-1
|
|
6
8
|
*/
|
|
7
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
-
import { createWriteStream } from "node:fs";
|
|
9
|
-
import * as fs from "node:fs/promises";
|
|
10
|
-
import { createRequire } from "node:module";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
9
|
import chalk from "chalk";
|
|
14
|
-
import { ulid } from "ulid";
|
|
15
|
-
// Read version from package.json for ACP client info
|
|
16
|
-
const require = createRequire(import.meta.url);
|
|
17
|
-
const { version: packageVersion } = require("../../../package.json");
|
|
18
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
-
const __dirname = path.dirname(__filename);
|
|
20
|
-
const DEFAULT_KSPEC_CLI_PATH = path.resolve(__dirname, "..", "index.js");
|
|
21
|
-
import { registerAdapter, resolveAdapter, } from "../../agents/index.js";
|
|
22
|
-
import { spawnAndInitialize } from "../../agents/spawner.js";
|
|
23
|
-
import { initContext, loadAllItems, loadMetaContext, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
|
|
24
|
-
import { resolveSkillReferenceTokensForPlatform } from "../../parser/skill-render.js";
|
|
25
|
-
import { buildWrapUpContext, createCliRenderer, createTranslator, DEFAULT_SUBAGENT_PREFIX, DEFAULT_WRAPUP_TIMEOUT, formatJsonSection, RALPH_PROMPT_TIMEOUT, runSubagent, runWrapUpAgent, truncatePromptIfNeeded, WRAPUP_AGENT_PREFIX, } from "../../ralph/index.js";
|
|
26
|
-
import { appendEvent, closeSession, createSessionWithBudget, getSessionBudgetPath, getSessionDir, injectEnvForAdapter, isEndLoopRequested, removeEnvForAdapter, requestEndLoop, resetBudget, saveSessionContext, } from "../../sessions/index.js";
|
|
27
|
-
import { errors } from "../../strings/index.js";
|
|
28
|
-
import { getCurrentBranch } from "../../utils/git.js";
|
|
29
10
|
import { EXIT_CODES } from "../exit-codes.js";
|
|
30
|
-
import { error, info, success, warn } from "../output.js";
|
|
31
|
-
import { gatherSessionContext, } from "./session.js";
|
|
32
|
-
/**
|
|
33
|
-
* Parse and validate --tasks flag value.
|
|
34
|
-
* Returns resolved ULIDs for the specified task refs.
|
|
35
|
-
* AC: @cli-ralph ac-21
|
|
36
|
-
*
|
|
37
|
-
* @throws Error if any ref cannot be resolved or is not a task
|
|
38
|
-
*/
|
|
39
|
-
async function parseExplicitTasks(ctx, tasksArg) {
|
|
40
|
-
const refs = tasksArg.split(",").map((r) => r.trim()).filter(Boolean);
|
|
41
|
-
if (refs.length === 0) {
|
|
42
|
-
throw new Error("--tasks requires at least one task reference");
|
|
43
|
-
}
|
|
44
|
-
// Load tasks and items for resolution
|
|
45
|
-
const tasks = await loadAllTasks(ctx);
|
|
46
|
-
const items = await loadAllItems(ctx);
|
|
47
|
-
const index = new ReferenceIndex(tasks, items);
|
|
48
|
-
const ulids = [];
|
|
49
|
-
for (const ref of refs) {
|
|
50
|
-
const result = index.resolve(ref);
|
|
51
|
-
if (!result.ok) {
|
|
52
|
-
throw new Error(`Cannot resolve task reference: ${ref}`);
|
|
53
|
-
}
|
|
54
|
-
// Verify it's a task (not a spec item)
|
|
55
|
-
const task = tasks.find((t) => t._ulid === result.ulid);
|
|
56
|
-
if (!task) {
|
|
57
|
-
throw new Error(`Reference ${ref} is not a task`);
|
|
58
|
-
}
|
|
59
|
-
ulids.push(result.ulid);
|
|
60
|
-
}
|
|
61
|
-
return { refs, ulids };
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Filter session context to only include tasks from explicit scope.
|
|
65
|
-
* AC: @cli-ralph ac-21
|
|
66
|
-
*/
|
|
67
|
-
function filterByExplicitTasks(ctx, scope) {
|
|
68
|
-
// Task refs in context are short ULIDs (variable length from shortUlid())
|
|
69
|
-
// Check if the context ref is a prefix of any explicit ULID
|
|
70
|
-
const matchesScope = (taskRef) => {
|
|
71
|
-
return scope.ulids.some((ulid) => ulid.startsWith(taskRef));
|
|
72
|
-
};
|
|
73
|
-
return {
|
|
74
|
-
...ctx,
|
|
75
|
-
active_tasks: ctx.active_tasks.filter((t) => matchesScope(t.ref)),
|
|
76
|
-
pending_review_tasks: ctx.pending_review_tasks.filter((t) => matchesScope(t.ref)),
|
|
77
|
-
ready_tasks: ctx.ready_tasks.filter((t) => matchesScope(t.ref)),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Check if all explicit tasks are completed or blocked.
|
|
82
|
-
* AC: @cli-ralph ac-21
|
|
83
|
-
*/
|
|
84
|
-
async function allExplicitTasksDone(ctx, scope) {
|
|
85
|
-
const tasks = await loadAllTasks(ctx);
|
|
86
|
-
const statuses = new Map();
|
|
87
|
-
for (const ulid of scope.ulids) {
|
|
88
|
-
const task = tasks.find((t) => t._ulid === ulid);
|
|
89
|
-
if (task) {
|
|
90
|
-
statuses.set(ulid.slice(0, 8), task.status);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Check if all are completed or blocked
|
|
94
|
-
const done = scope.ulids.every((ulid) => {
|
|
95
|
-
const status = statuses.get(ulid.slice(0, 8));
|
|
96
|
-
return status === "completed" || status === "blocked";
|
|
97
|
-
});
|
|
98
|
-
return { done, statuses };
|
|
99
|
-
}
|
|
100
|
-
const FALLBACK_CORE_SKILLS = new Set(["task-work", "reflect", "review"]);
|
|
101
|
-
const ADAPTER_VALIDATION_PROBES = [["--help"], ["--version"]];
|
|
102
|
-
const TERMINAL_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
103
|
-
const TOOL_OUTPUT_DIR = "tool-output";
|
|
104
|
-
const RECENT_TASK_REF_LIMIT = 50;
|
|
105
|
-
export function pushRecentTaskRef(recentTaskRefs, ref, limit = RECENT_TASK_REF_LIMIT) {
|
|
106
|
-
const existingIndex = recentTaskRefs.indexOf(ref);
|
|
107
|
-
if (existingIndex !== -1) {
|
|
108
|
-
recentTaskRefs.splice(existingIndex, 1);
|
|
109
|
-
}
|
|
110
|
-
recentTaskRefs.push(ref);
|
|
111
|
-
if (recentTaskRefs.length > limit) {
|
|
112
|
-
recentTaskRefs.splice(0, recentTaskRefs.length - limit);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
export function setSessionIteration(sessionIterationMap, sessionId, iteration) {
|
|
116
|
-
sessionIterationMap.clear();
|
|
117
|
-
sessionIterationMap.set(sessionId, iteration);
|
|
118
|
-
}
|
|
119
|
-
export function disposeSpawnedAgent(spawned) {
|
|
120
|
-
if (!spawned) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
// Remove callbacks first so captured loop state can be garbage-collected.
|
|
124
|
-
spawned.client.removeAllListeners();
|
|
125
|
-
spawned.kill();
|
|
126
|
-
if (!spawned.client.isClosed()) {
|
|
127
|
-
spawned.client.close();
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Map adapter IDs to prompt rendering platforms.
|
|
133
|
-
*/
|
|
134
|
-
export function getPromptPlatformForAdapter(adapterId) {
|
|
135
|
-
switch (adapterId) {
|
|
136
|
-
case "claude-agent-acp":
|
|
137
|
-
case "claude-code-acp":
|
|
138
|
-
return "claude-code";
|
|
139
|
-
case "codex-acp":
|
|
140
|
-
return "codex";
|
|
141
|
-
default:
|
|
142
|
-
return "unknown";
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Build skill origin map from meta skills.
|
|
147
|
-
*/
|
|
148
|
-
async function loadSkillOriginsForRalph(ctx) {
|
|
149
|
-
const meta = await loadMetaContext(ctx);
|
|
150
|
-
const origins = new Map();
|
|
151
|
-
for (const skill of meta.skills) {
|
|
152
|
-
origins.set(skill.id, skill.origin);
|
|
153
|
-
}
|
|
154
|
-
// Fallback for core skills frequently used by ralph, even if core skills
|
|
155
|
-
// were not loaded into project meta for any reason.
|
|
156
|
-
for (const coreSkill of FALLBACK_CORE_SKILLS) {
|
|
157
|
-
if (!origins.has(coreSkill)) {
|
|
158
|
-
origins.set(coreSkill, "core");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return origins;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Normalize legacy literal invocation syntax for a target platform.
|
|
165
|
-
* Keeps backward compatibility for existing slash-style config values.
|
|
166
|
-
*/
|
|
167
|
-
function normalizeLegacyInvocation(invocation, platform) {
|
|
168
|
-
if (platform === "codex") {
|
|
169
|
-
if (/^\/kspec:([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
170
|
-
return invocation.replace(/^\/kspec:([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `$kspec-${skillId}`);
|
|
171
|
-
}
|
|
172
|
-
if (/^\/([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
173
|
-
return invocation.replace(/^\/([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `$${skillId}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (platform === "claude-code") {
|
|
177
|
-
if (/^\$kspec-([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
178
|
-
return invocation.replace(/^\$kspec-([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `/kspec:${skillId}`);
|
|
179
|
-
}
|
|
180
|
-
if (/^\$([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
|
|
181
|
-
return invocation.replace(/^\$([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `/${skillId}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return invocation;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Resolve configured skill invocation string for a specific platform.
|
|
188
|
-
* Supports portable {skill:<id>} syntax and legacy literal strings.
|
|
189
|
-
*/
|
|
190
|
-
export function resolveRalphSkillInvocation(invocation, platform, skillOrigins) {
|
|
191
|
-
if (platform === "unknown") {
|
|
192
|
-
return invocation;
|
|
193
|
-
}
|
|
194
|
-
const tokenResolved = resolveSkillReferenceTokensForPlatform(invocation, platform, skillOrigins);
|
|
195
|
-
if (tokenResolved !== invocation) {
|
|
196
|
-
return tokenResolved;
|
|
197
|
-
}
|
|
198
|
-
return normalizeLegacyInvocation(invocation, platform);
|
|
199
|
-
}
|
|
200
|
-
// AC: @ralph-skill-delegation ac-1, ac-2, ac-3
|
|
201
|
-
function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, skillTaskWork, focus, explicitTaskScope) {
|
|
202
|
-
const focusSection = focus
|
|
203
|
-
? `
|
|
204
|
-
## Session Focus (applies to ALL iterations)
|
|
205
|
-
|
|
206
|
-
> **${focus}**
|
|
207
|
-
|
|
208
|
-
Keep this focus in mind throughout your work. It takes priority over default task selection.
|
|
209
|
-
`
|
|
210
|
-
: "";
|
|
211
|
-
// AC: @cli-ralph ac-21 - Explicit task scope indicator in prompt
|
|
212
|
-
const taskScopeSection = explicitTaskScope
|
|
213
|
-
? `
|
|
214
|
-
## Explicit Task Scope
|
|
215
|
-
|
|
216
|
-
This session is scoped to specific tasks: ${explicitTaskScope.refs.join(", ")}
|
|
217
|
-
|
|
218
|
-
**Only work on these tasks.** The loop will exit when all listed tasks are completed or blocked.
|
|
219
|
-
`
|
|
220
|
-
: "";
|
|
221
|
-
const modeDescription = explicitTaskScope
|
|
222
|
-
? "Loop mode means: no confirmations, auto-resolve decisions, explicit task scope (only the listed tasks)."
|
|
223
|
-
: "Loop mode means: no confirmations, auto-resolve decisions, automation-eligible tasks only.";
|
|
224
|
-
const stateSection = formatJsonSection(sessionCtx, "Current State", `kspec session start --json`);
|
|
225
|
-
const sections = [stateSection.section];
|
|
226
|
-
const prompt = `# Kspec Automation Session - Task Work
|
|
227
|
-
|
|
228
|
-
**Session ID:** \`${sessionId}\`
|
|
229
|
-
**Iteration:** ${iteration} of ${maxLoops}
|
|
230
|
-
**Mode:** Automated (no human in the loop)
|
|
231
|
-
${focusSection}${taskScopeSection}
|
|
232
|
-
|
|
233
|
-
${stateSection.text}
|
|
234
|
-
|
|
235
|
-
## Instructions
|
|
236
|
-
|
|
237
|
-
Run the task-work skill in loop mode:
|
|
238
|
-
|
|
239
|
-
\`\`\`
|
|
240
|
-
${skillTaskWork} loop
|
|
241
|
-
\`\`\`
|
|
242
|
-
|
|
243
|
-
${modeDescription}
|
|
244
|
-
|
|
245
|
-
**Normal flow:** Work on a task, create a PR, then stop responding. Ralph continues automatically —
|
|
246
|
-
it checks for remaining eligible tasks at the start of each iteration and exits the loop itself when none remain.
|
|
247
|
-
|
|
248
|
-
**Do NOT call \`end-loop\` after completing a task.** Simply stop responding.
|
|
249
|
-
\`end-loop\` is a rare escape hatch for when work is stalling across multiple iterations with no progress — not a normal exit path.
|
|
250
|
-
`;
|
|
251
|
-
return truncatePromptIfNeeded(prompt, sections);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Build the reflect prompt sent after task-work completes.
|
|
255
|
-
* Ralph sends this as a separate prompt to ensure reflection always happens.
|
|
256
|
-
*/
|
|
257
|
-
function buildReflectPrompt(iteration, maxLoops, sessionId, skillReflect) {
|
|
258
|
-
const isFinal = iteration === maxLoops;
|
|
259
|
-
return `# Kspec Automation Session - Reflection
|
|
260
|
-
|
|
261
|
-
**Session ID:** \`${sessionId}\`
|
|
262
|
-
**Iteration:** ${iteration} of ${maxLoops}
|
|
263
|
-
**Phase:** Post-task reflection
|
|
264
|
-
|
|
265
|
-
## Instructions
|
|
266
|
-
|
|
267
|
-
Run the reflect skill in loop mode:
|
|
268
|
-
|
|
269
|
-
\`\`\`
|
|
270
|
-
${skillReflect} loop
|
|
271
|
-
\`\`\`
|
|
272
|
-
|
|
273
|
-
Loop mode means: high-confidence captures only, must search existing before capturing, no user prompts.
|
|
274
|
-
${isFinal
|
|
275
|
-
? `
|
|
276
|
-
**FINAL ITERATION** - This is the last chance to capture insights from this session.
|
|
277
|
-
`
|
|
278
|
-
: ""}
|
|
279
|
-
Exit when reflection is complete.
|
|
280
|
-
`;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Check whether an adapter package appears to be installed and executable.
|
|
284
|
-
* Uses multiple non-installing probes because CLIs differ on supported flags.
|
|
285
|
-
*/
|
|
286
|
-
export function isAdapterPackageAvailable(adapterPackage, runner = spawnSync) {
|
|
287
|
-
for (const probeArgs of ADAPTER_VALIDATION_PROBES) {
|
|
288
|
-
const result = runner("npx", ["--no-install", adapterPackage, ...probeArgs], {
|
|
289
|
-
encoding: "utf-8",
|
|
290
|
-
stdio: "pipe",
|
|
291
|
-
});
|
|
292
|
-
if (result.status === 0) {
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Validate that the specified ACP adapter package exists.
|
|
300
|
-
* Uses npx --no-install probes to check both global and local node_modules.
|
|
301
|
-
*
|
|
302
|
-
* @throws {Error} Never throws - exits process with code 3 if validation fails
|
|
303
|
-
*/
|
|
304
|
-
function validateAdapter(adapterPackage, adapterId) {
|
|
305
|
-
if (!isAdapterPackageAvailable(adapterPackage)) {
|
|
306
|
-
const label = adapterId && adapterId !== adapterPackage
|
|
307
|
-
? `${adapterId} (${adapterPackage})`
|
|
308
|
-
: adapterPackage;
|
|
309
|
-
error(`Adapter not found: ${label}. Install with: npm install -g ${adapterPackage}`);
|
|
310
|
-
process.exit(EXIT_CODES.NOT_FOUND);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
function sanitizeToolCallId(toolCallId) {
|
|
314
|
-
const raw = String(toolCallId).trim();
|
|
315
|
-
if (!raw) {
|
|
316
|
-
return "tool-call";
|
|
317
|
-
}
|
|
318
|
-
return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
319
|
-
}
|
|
320
|
-
function updateStreamPreview(state, chunk, maxPreviewBytes) {
|
|
321
|
-
state.bytes += chunk.length;
|
|
322
|
-
const remaining = maxPreviewBytes - state.previewBytes;
|
|
323
|
-
if (remaining <= 0) {
|
|
324
|
-
state.truncated = true;
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (chunk.length > remaining) {
|
|
328
|
-
state.previewParts.push(chunk.subarray(0, remaining).toString("utf-8"));
|
|
329
|
-
state.previewBytes += remaining;
|
|
330
|
-
state.truncated = true;
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
state.previewParts.push(chunk.toString("utf-8"));
|
|
334
|
-
state.previewBytes += chunk.length;
|
|
335
|
-
}
|
|
336
|
-
function closeStream(stream) {
|
|
337
|
-
if (!stream) {
|
|
338
|
-
return Promise.resolve();
|
|
339
|
-
}
|
|
340
|
-
return new Promise((resolve, reject) => {
|
|
341
|
-
const onError = (err) => {
|
|
342
|
-
stream.off("finish", onFinish);
|
|
343
|
-
reject(err);
|
|
344
|
-
};
|
|
345
|
-
const onFinish = () => {
|
|
346
|
-
stream.off("error", onError);
|
|
347
|
-
resolve();
|
|
348
|
-
};
|
|
349
|
-
stream.once("error", onError);
|
|
350
|
-
stream.once("finish", onFinish);
|
|
351
|
-
stream.end();
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Execute terminal/run request with bounded in-memory preview and streamed
|
|
356
|
-
* session artifacts for full stdout/stderr retention.
|
|
357
|
-
*/
|
|
358
|
-
export async function runTerminalCommandWithArtifacts(options) {
|
|
359
|
-
const previewMaxBytes = options.previewMaxBytes ?? TERMINAL_PREVIEW_MAX_BYTES;
|
|
360
|
-
const shouldWriteArtifacts = Boolean(options.specDir && options.sessionId);
|
|
361
|
-
let stdoutPath;
|
|
362
|
-
let stderrPath;
|
|
363
|
-
if (shouldWriteArtifacts) {
|
|
364
|
-
const outputDir = path.join(getSessionDir(options.specDir, options.sessionId), TOOL_OUTPUT_DIR);
|
|
365
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
366
|
-
const safeToolCallId = sanitizeToolCallId(options.toolCallId);
|
|
367
|
-
stdoutPath = path.join(outputDir, `${safeToolCallId}.stdout.log`);
|
|
368
|
-
stderrPath = path.join(outputDir, `${safeToolCallId}.stderr.log`);
|
|
369
|
-
}
|
|
370
|
-
const stdoutState = {
|
|
371
|
-
bytes: 0,
|
|
372
|
-
previewBytes: 0,
|
|
373
|
-
previewParts: [],
|
|
374
|
-
truncated: false,
|
|
375
|
-
stream: stdoutPath ? createWriteStream(stdoutPath) : undefined,
|
|
376
|
-
};
|
|
377
|
-
const stderrState = {
|
|
378
|
-
bytes: 0,
|
|
379
|
-
previewBytes: 0,
|
|
380
|
-
previewParts: [],
|
|
381
|
-
truncated: false,
|
|
382
|
-
stream: stderrPath ? createWriteStream(stderrPath) : undefined,
|
|
383
|
-
};
|
|
384
|
-
return await new Promise((resolve, reject) => {
|
|
385
|
-
let settled = false;
|
|
386
|
-
const child = spawn(options.command, [], {
|
|
387
|
-
cwd: options.cwd,
|
|
388
|
-
shell: true,
|
|
389
|
-
timeout: options.timeout,
|
|
390
|
-
});
|
|
391
|
-
const finalize = async (exitCode, errorMessage) => {
|
|
392
|
-
if (settled) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
settled = true;
|
|
396
|
-
if (errorMessage) {
|
|
397
|
-
const errChunk = Buffer.from(errorMessage, "utf-8");
|
|
398
|
-
stderrState.stream?.write(errChunk);
|
|
399
|
-
updateStreamPreview(stderrState, errChunk, previewMaxBytes);
|
|
400
|
-
}
|
|
401
|
-
try {
|
|
402
|
-
await Promise.all([
|
|
403
|
-
closeStream(stdoutState.stream),
|
|
404
|
-
closeStream(stderrState.stream),
|
|
405
|
-
]);
|
|
406
|
-
}
|
|
407
|
-
catch (streamErr) {
|
|
408
|
-
reject(streamErr);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
resolve({
|
|
412
|
-
stdout: stdoutState.previewParts.join(""),
|
|
413
|
-
stderr: stderrState.previewParts.join(""),
|
|
414
|
-
exitCode,
|
|
415
|
-
stdout_path: stdoutPath,
|
|
416
|
-
stderr_path: stderrPath,
|
|
417
|
-
stdout_bytes: stdoutState.bytes,
|
|
418
|
-
stderr_bytes: stderrState.bytes,
|
|
419
|
-
preview_truncated: stdoutState.truncated || stderrState.truncated,
|
|
420
|
-
});
|
|
421
|
-
};
|
|
422
|
-
child.stdout?.on("data", (data) => {
|
|
423
|
-
const chunk = Buffer.isBuffer(data)
|
|
424
|
-
? data
|
|
425
|
-
: Buffer.from(String(data), "utf-8");
|
|
426
|
-
stdoutState.stream?.write(chunk);
|
|
427
|
-
updateStreamPreview(stdoutState, chunk, previewMaxBytes);
|
|
428
|
-
});
|
|
429
|
-
child.stderr?.on("data", (data) => {
|
|
430
|
-
const chunk = Buffer.isBuffer(data)
|
|
431
|
-
? data
|
|
432
|
-
: Buffer.from(String(data), "utf-8");
|
|
433
|
-
stderrState.stream?.write(chunk);
|
|
434
|
-
updateStreamPreview(stderrState, chunk, previewMaxBytes);
|
|
435
|
-
});
|
|
436
|
-
child.on("close", (code) => {
|
|
437
|
-
void finalize(code ?? 1);
|
|
438
|
-
});
|
|
439
|
-
child.on("error", (err) => {
|
|
440
|
-
void finalize(1, err.message);
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
// ─── Tool Request Handler ────────────────────────────────────────────────────
|
|
445
|
-
/**
|
|
446
|
-
* Handle tool requests from ACP agent.
|
|
447
|
-
* Implements file operations, terminal commands, and permission handling.
|
|
448
|
-
*/
|
|
449
|
-
async function handleRequest(client, id, method, params, options) {
|
|
450
|
-
try {
|
|
451
|
-
switch (method) {
|
|
452
|
-
case "session/request_permission": {
|
|
453
|
-
const p = params;
|
|
454
|
-
// In yolo mode, auto-approve all permissions
|
|
455
|
-
// In normal mode, would need to implement permission UI
|
|
456
|
-
const permissionOptions = p.options || [];
|
|
457
|
-
if (options.yolo) {
|
|
458
|
-
// Find an "allow" option (prefer allow_always, then allow_once)
|
|
459
|
-
const allowOption = permissionOptions.find((o) => o.kind === "allow_always") ||
|
|
460
|
-
permissionOptions.find((o) => o.kind === "allow_once");
|
|
461
|
-
if (allowOption) {
|
|
462
|
-
client.respondPermission(id, {
|
|
463
|
-
outcome: { outcome: "selected", optionId: allowOption.optionId },
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
// No allow option available - cancel
|
|
468
|
-
client.respondPermission(id, { outcome: { outcome: "cancelled" } });
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
// TODO: Implement permission prompting
|
|
473
|
-
client.respondPermission(id, { outcome: { outcome: "cancelled" } });
|
|
474
|
-
}
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
case "file/read": {
|
|
478
|
-
const p = params;
|
|
479
|
-
const content = await fs.readFile(p.path, "utf-8");
|
|
480
|
-
client.respondReadTextFile(id, { content });
|
|
481
|
-
break;
|
|
482
|
-
}
|
|
483
|
-
case "file/write": {
|
|
484
|
-
const p = params;
|
|
485
|
-
await fs.mkdir(path.dirname(p.path), { recursive: true });
|
|
486
|
-
await fs.writeFile(p.path, p.content, "utf-8");
|
|
487
|
-
client.respondWriteTextFile(id, {});
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
case "terminal/run": {
|
|
491
|
-
// Custom method (not part of ACP spec - ACP uses createTerminal instead)
|
|
492
|
-
// TODO: Consider migrating to standard ACP terminal methods
|
|
493
|
-
const p = params;
|
|
494
|
-
const command = p.command;
|
|
495
|
-
const cwd = p.cwd || process.cwd();
|
|
496
|
-
const timeout = p.timeout || 60000;
|
|
497
|
-
const result = await runTerminalCommandWithArtifacts({
|
|
498
|
-
command,
|
|
499
|
-
cwd,
|
|
500
|
-
timeout,
|
|
501
|
-
toolCallId: id,
|
|
502
|
-
specDir: options.specDir,
|
|
503
|
-
sessionId: options.sessionId,
|
|
504
|
-
});
|
|
505
|
-
// Using generic respond() since this is a custom method
|
|
506
|
-
client.respond(id, result);
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
default:
|
|
510
|
-
// Unknown method - return error
|
|
511
|
-
client.respondError(id, -32601, `Method not found: ${method}`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
catch (err) {
|
|
515
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
516
|
-
client.respondError(id, -32000, message);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// ─── Subagent Support ─────────────────────────────────────────────────────────
|
|
520
|
-
/**
|
|
521
|
-
* Build context for a PR review subagent.
|
|
522
|
-
* AC: @ralph-subagent-spawning ac-10
|
|
523
|
-
*/
|
|
524
|
-
async function buildSubagentContext(ctx, taskRef) {
|
|
525
|
-
// Load all tasks and items
|
|
526
|
-
const tasks = await loadAllTasks(ctx);
|
|
527
|
-
const items = await loadAllItems(ctx);
|
|
528
|
-
const index = new ReferenceIndex(tasks, items);
|
|
529
|
-
// Resolve task reference
|
|
530
|
-
const taskResult = index.resolve(taskRef);
|
|
531
|
-
if (!taskResult.ok) {
|
|
532
|
-
throw new Error(`Task not found: ${taskRef}`);
|
|
533
|
-
}
|
|
534
|
-
const task = tasks.find((t) => t._ulid === taskResult.ulid);
|
|
535
|
-
if (!task) {
|
|
536
|
-
throw new Error(`Task not found by ULID: ${taskResult.ulid}`);
|
|
537
|
-
}
|
|
538
|
-
// Get linked spec with ACs if spec_ref exists
|
|
539
|
-
let specWithACs = null;
|
|
540
|
-
if (task.spec_ref) {
|
|
541
|
-
const specResult = index.resolve(task.spec_ref);
|
|
542
|
-
if (specResult.ok) {
|
|
543
|
-
const item = items.find((i) => i._ulid === specResult.ulid);
|
|
544
|
-
if (item) {
|
|
545
|
-
specWithACs = item;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
// Get git branch
|
|
550
|
-
const gitBranch = getCurrentBranch(ctx.rootDir) || "unknown";
|
|
551
|
-
return {
|
|
552
|
-
taskRef,
|
|
553
|
-
taskDetails: task,
|
|
554
|
-
specWithACs,
|
|
555
|
-
gitBranch,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Get the current status of a task.
|
|
560
|
-
* AC: @ralph-subagent-spawning ac-12
|
|
561
|
-
*/
|
|
562
|
-
function runKspecCli(args) {
|
|
563
|
-
const cliPath = process.env.KSPEC_CLI_PATH || DEFAULT_KSPEC_CLI_PATH;
|
|
564
|
-
return spawnSync(process.execPath, [cliPath, ...args], {
|
|
565
|
-
encoding: "utf-8",
|
|
566
|
-
stdio: "pipe",
|
|
567
|
-
cwd: process.cwd(),
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
function getTaskStatus(taskRef) {
|
|
571
|
-
const result = runKspecCli(["task", "get", taskRef, "--json"]);
|
|
572
|
-
if (result.status !== 0) {
|
|
573
|
-
warn(`Failed to check task status for ${taskRef}: ${result.stderr}`);
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
try {
|
|
577
|
-
return JSON.parse(result.stdout).status;
|
|
578
|
-
}
|
|
579
|
-
catch {
|
|
580
|
-
warn(`Failed to parse task status for ${taskRef}`);
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Mark a task as needing review due to subagent timeout.
|
|
586
|
-
* AC: @ralph-subagent-spawning ac-9
|
|
587
|
-
*/
|
|
588
|
-
async function markTaskNeedsReview(taskRef, reason) {
|
|
589
|
-
// Use kspec CLI to set automation status
|
|
590
|
-
const result = runKspecCli(["task", "set", taskRef, "--automation", "needs_review", "--reason", reason]);
|
|
591
|
-
if (result.status !== 0) {
|
|
592
|
-
warn(`Failed to mark task ${taskRef} as needs_review: ${result.stderr}`);
|
|
593
|
-
}
|
|
594
|
-
// Add a note explaining the timeout
|
|
595
|
-
const noteResult = runKspecCli(["task", "note", taskRef, `[RALPH SUBAGENT] ${reason}`]);
|
|
596
|
-
if (noteResult.status !== 0) {
|
|
597
|
-
warn(`Failed to add timeout note to task ${taskRef}: ${noteResult.stderr}`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Post a comment on the open PR for a task's branch, noting incomplete review.
|
|
602
|
-
* Uses `gh pr list --head <branch>` to find the PR and add a warning.
|
|
603
|
-
*/
|
|
604
|
-
async function commentOnPRReviewIncomplete(branch, reason) {
|
|
605
|
-
if (!branch || branch === "unknown") {
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
const prListResult = spawnSync("gh", ["pr", "list", "--state", "open", "--head", branch, "--json", "number", "--jq", ".[0].number"], { encoding: "utf-8", stdio: "pipe" });
|
|
609
|
-
const prNumber = prListResult.stdout?.trim();
|
|
610
|
-
if (!prNumber || prListResult.status !== 0) {
|
|
611
|
-
// No open PR found — may already be merged or branch has no PR
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
const body = `⚠️ **Review incomplete**: ${reason}\n\nThis PR was not fully reviewed by the ralph review subagent. Manual review recommended before merging.`;
|
|
615
|
-
const commentResult = spawnSync("gh", ["pr", "comment", prNumber, "--body", body], { encoding: "utf-8", stdio: "pipe" });
|
|
616
|
-
if (commentResult.status !== 0) {
|
|
617
|
-
warn(`Failed to comment on PR #${prNumber}: ${commentResult.stderr}`);
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
620
|
-
info(`${DEFAULT_SUBAGENT_PREFIX} Posted review-incomplete comment on PR #${prNumber}`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Handle failed iteration by tracking per-task failures and escalating at threshold.
|
|
625
|
-
* AC: @loop-mode-error-handling ac-1, ac-2, ac-3, ac-4, ac-5, ac-8
|
|
626
|
-
*/
|
|
627
|
-
async function handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDescription) {
|
|
628
|
-
if (tasksInProgressAtStart.length === 0) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
// Re-load current tasks to check progress
|
|
632
|
-
const currentTasks = await loadAllTasks(ctx);
|
|
633
|
-
const index = new ReferenceIndex(currentTasks, await loadAllItems(ctx));
|
|
634
|
-
// Convert ActiveTaskSummary to Task-like objects for processing
|
|
635
|
-
const tasksInProgressFull = tasksInProgressAtStart
|
|
636
|
-
.map((summary) => {
|
|
637
|
-
const resolved = index.resolve(summary.ref);
|
|
638
|
-
if (!resolved.ok)
|
|
639
|
-
return undefined;
|
|
640
|
-
// Check if the resolved item is a task (not a spec item or meta item)
|
|
641
|
-
const item = resolved.item;
|
|
642
|
-
if (!("status" in item))
|
|
643
|
-
return undefined; // Spec items don't have status
|
|
644
|
-
return currentTasks.find((t) => t._ulid === resolved.ulid);
|
|
645
|
-
})
|
|
646
|
-
.filter((t) => t !== undefined && t.status === "in_progress");
|
|
647
|
-
if (tasksInProgressFull.length === 0) {
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
// Process failures
|
|
651
|
-
const { processFailedIteration, createFailureNote, getTaskFailureCount } = await import("../../ralph/index.js");
|
|
652
|
-
const results = processFailedIteration(tasksInProgressFull, currentTasks, iterationStartTime, errorDescription);
|
|
653
|
-
// Add notes and escalate tasks
|
|
654
|
-
for (const result of results) {
|
|
655
|
-
const taskRef = result.taskRef;
|
|
656
|
-
const task = currentTasks.find((t) => t._ulid === taskRef);
|
|
657
|
-
if (!task)
|
|
658
|
-
continue;
|
|
659
|
-
const priorCount = result.failureCount - 1;
|
|
660
|
-
const noteContent = createFailureNote(taskRef, errorDescription, priorCount);
|
|
661
|
-
// Add LOOP-FAIL note
|
|
662
|
-
const noteResult = spawnSync("kspec", ["task", "note", `@${taskRef}`, noteContent], {
|
|
663
|
-
encoding: "utf-8",
|
|
664
|
-
stdio: "pipe",
|
|
665
|
-
cwd: process.cwd(),
|
|
666
|
-
});
|
|
667
|
-
if (noteResult.status !== 0) {
|
|
668
|
-
warn(`Failed to add failure note to task ${taskRef}: ${noteResult.stderr}`);
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
// AC: @loop-mode-error-handling ac-5 - Escalate at threshold
|
|
672
|
-
if (result.escalated) {
|
|
673
|
-
const escalateResult = spawnSync("kspec", [
|
|
674
|
-
"task",
|
|
675
|
-
"set",
|
|
676
|
-
`@${taskRef}`,
|
|
677
|
-
"--automation",
|
|
678
|
-
"needs_review",
|
|
679
|
-
"--reason",
|
|
680
|
-
`Loop mode: 3 consecutive failures without progress`,
|
|
681
|
-
], {
|
|
682
|
-
encoding: "utf-8",
|
|
683
|
-
stdio: "pipe",
|
|
684
|
-
cwd: process.cwd(),
|
|
685
|
-
});
|
|
686
|
-
if (escalateResult.status !== 0) {
|
|
687
|
-
warn(`Failed to escalate task ${taskRef}: ${escalateResult.stderr}`);
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
info(`Escalated task ${taskRef} to automation:needs_review after 3 failures`);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Process pending_review tasks by spawning subagents.
|
|
697
|
-
* AC: @ralph-subagent-spawning ac-6, ac-8
|
|
698
|
-
*/
|
|
699
|
-
async function processPendingReviewTasks(ctx, adapter, pendingReviewTasks, options, consecutiveFailures) {
|
|
700
|
-
if (pendingReviewTasks.length === 0) {
|
|
701
|
-
return true;
|
|
702
|
-
}
|
|
703
|
-
// Visual separator for subagent section
|
|
704
|
-
console.log("");
|
|
705
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
706
|
-
console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Processing Pending Review Tasks`));
|
|
707
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
708
|
-
console.log("");
|
|
709
|
-
info(`${DEFAULT_SUBAGENT_PREFIX} Found ${pendingReviewTasks.length} pending_review task(s)`);
|
|
710
|
-
// AC: @ralph-subagent-spawning ac-6 - Process one at a time
|
|
711
|
-
for (const task of pendingReviewTasks) {
|
|
712
|
-
info(`${DEFAULT_SUBAGENT_PREFIX} Processing: ${task.ref} - ${task.title}`);
|
|
713
|
-
try {
|
|
714
|
-
// Build context for this task
|
|
715
|
-
const subagentCtx = await buildSubagentContext(ctx, task.ref);
|
|
716
|
-
// AC: @ralph-subagent-spawning ac-1, ac-3 - Spawn and wait
|
|
717
|
-
const result = await runSubagent(adapter, subagentCtx, {
|
|
718
|
-
timeout: options.subagentTimeout,
|
|
719
|
-
outputPrefix: DEFAULT_SUBAGENT_PREFIX,
|
|
720
|
-
skillName: options.prReviewSkillName,
|
|
721
|
-
}, {
|
|
722
|
-
yolo: options.yolo,
|
|
723
|
-
cwd: options.cwd,
|
|
724
|
-
extraArgs: options.autoApproveArgs,
|
|
725
|
-
handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, {
|
|
726
|
-
yolo: options.yolo,
|
|
727
|
-
specDir: options.specDir,
|
|
728
|
-
sessionId: options.sessionId,
|
|
729
|
-
}),
|
|
730
|
-
});
|
|
731
|
-
if (result.timedOut) {
|
|
732
|
-
// AC: @ralph-subagent-spawning ac-9
|
|
733
|
-
warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent timed out for ${task.ref}`);
|
|
734
|
-
const timeoutMinutes = Math.round(options.subagentTimeout / 60000);
|
|
735
|
-
await markTaskNeedsReview(task.ref, `Subagent timed out after ${timeoutMinutes} minutes`);
|
|
736
|
-
await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent timed out after ${timeoutMinutes} minutes for task ${task.ref}.`);
|
|
737
|
-
consecutiveFailures.count++;
|
|
738
|
-
}
|
|
739
|
-
else if (!result.success) {
|
|
740
|
-
// AC: @ralph-subagent-spawning ac-7
|
|
741
|
-
error(`${DEFAULT_SUBAGENT_PREFIX} Subagent failed for ${task.ref}: ${result.error}`);
|
|
742
|
-
await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent failed for task ${task.ref}: ${result.error}`);
|
|
743
|
-
consecutiveFailures.count++;
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
// AC: @ralph-subagent-spawning ac-12 - Verify task outcome
|
|
747
|
-
const currentStatus = getTaskStatus(task.ref);
|
|
748
|
-
if (currentStatus === "completed") {
|
|
749
|
-
success(`${DEFAULT_SUBAGENT_PREFIX} Completed: ${task.ref}`);
|
|
750
|
-
consecutiveFailures.count = 0;
|
|
751
|
-
}
|
|
752
|
-
else if (currentStatus === "needs_work") {
|
|
753
|
-
// Expected: reviewer found issues, kicked back to worker
|
|
754
|
-
info(`${DEFAULT_SUBAGENT_PREFIX} Review completed for ${task.ref} — issues found, kicked back to worker`);
|
|
755
|
-
// NOT a failure — the review worked correctly
|
|
756
|
-
consecutiveFailures.count = 0;
|
|
757
|
-
}
|
|
758
|
-
else if (currentStatus === "pending_review") {
|
|
759
|
-
// Subagent didn't transition or merge — count as soft failure
|
|
760
|
-
warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent completed but task ${task.ref} unchanged`);
|
|
761
|
-
await markTaskNeedsReview(task.ref, "Subagent completed but did not merge or kick back. Review required.");
|
|
762
|
-
consecutiveFailures.count++;
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
warn(`${DEFAULT_SUBAGENT_PREFIX} Task ${task.ref} in unexpected state: ${currentStatus}`);
|
|
766
|
-
consecutiveFailures.count++;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
// Check if we've hit max failures
|
|
770
|
-
if (consecutiveFailures.count >= options.maxFailures) {
|
|
771
|
-
error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
catch (err) {
|
|
776
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
777
|
-
error(`${DEFAULT_SUBAGENT_PREFIX} Error processing ${task.ref}: ${message}`);
|
|
778
|
-
consecutiveFailures.count++;
|
|
779
|
-
if (consecutiveFailures.count >= options.maxFailures) {
|
|
780
|
-
error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
// Visual separator at end of subagent section
|
|
786
|
-
console.log("");
|
|
787
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
788
|
-
console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Completed Review Processing`));
|
|
789
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
790
|
-
console.log("");
|
|
791
|
-
return true;
|
|
792
|
-
}
|
|
793
11
|
// ─── Command Registration ────────────────────────────────────────────────────
|
|
794
12
|
export function registerRalphCommand(program) {
|
|
795
13
|
const ralph = program
|
|
796
14
|
.command("ralph")
|
|
797
|
-
.description("
|
|
798
|
-
// end-loop subcommand -
|
|
799
|
-
// AC: @
|
|
15
|
+
.description("[deprecated] Use kspec agent instead");
|
|
16
|
+
// end-loop subcommand - deprecated
|
|
17
|
+
// AC: @ralph-replacement ac-1
|
|
800
18
|
ralph
|
|
801
19
|
.command("end-loop")
|
|
802
|
-
.description("
|
|
20
|
+
.description("[deprecated] Use kspec agent end-loop instead")
|
|
803
21
|
.option("--reason <reason>", "Reason for ending the loop")
|
|
804
|
-
.action(
|
|
805
|
-
|
|
806
|
-
const ctx = await initContext();
|
|
807
|
-
const sessionId = process.env.KSPEC_SESSION_ID;
|
|
808
|
-
if (!sessionId) {
|
|
809
|
-
// AC: @trait-error-guidance ac-1, ac-2
|
|
810
|
-
warn("No active ralph session detected (KSPEC_SESSION_ID not set).");
|
|
811
|
-
info("This command requires an active session. It is designed to be called by agents during a ralph loop.");
|
|
812
|
-
info("Suggestion: Ensure KSPEC_SESSION_ID is set, or start a session with: kspec session create --agent-type ralph");
|
|
813
|
-
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
// AC: @session-end-loop-signal ac-signal - Write end-loop state to session
|
|
817
|
-
const updated = await requestEndLoop(ctx.specDir, sessionId, options.reason);
|
|
818
|
-
if (!updated) {
|
|
819
|
-
// AC: @trait-error-guidance ac-1, ac-2
|
|
820
|
-
error(`Session not found: ${sessionId}`);
|
|
821
|
-
info("Suggestion: Check session ID with: kspec session log list");
|
|
822
|
-
process.exit(EXIT_CODES.NOT_FOUND);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
success("Loop end signal sent");
|
|
826
|
-
if (options.reason) {
|
|
827
|
-
info(`Reason: ${options.reason}`);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
catch (err) {
|
|
831
|
-
// AC: @trait-error-guidance ac-1
|
|
832
|
-
error("Failed to signal end-loop", err);
|
|
833
|
-
process.exit(EXIT_CODES.ERROR);
|
|
834
|
-
}
|
|
22
|
+
.action(() => {
|
|
23
|
+
showRalphDeprecationError();
|
|
835
24
|
});
|
|
836
|
-
// Main ralph run command (default behavior
|
|
25
|
+
// Main ralph run command (default behavior)
|
|
26
|
+
// AC: @ralph-replacement ac-1
|
|
837
27
|
ralph
|
|
838
28
|
.command("run", { isDefault: true })
|
|
839
|
-
.description("
|
|
29
|
+
.description("[deprecated] Use kspec agent dispatch start instead")
|
|
840
30
|
.argument("[args...]", "")
|
|
841
|
-
.
|
|
842
|
-
.
|
|
843
|
-
|
|
844
|
-
.option("--dry-run", "Show prompt without executing")
|
|
845
|
-
.option("--yolo", "Use dangerously-skip-permissions (default)", true)
|
|
846
|
-
.option("--no-yolo", "Require normal permission prompts")
|
|
847
|
-
.option("--subagent-timeout <minutes>", "Review subagent timeout in minutes", "20")
|
|
848
|
-
.option("--adapter <id>", "Agent adapter to use", "claude-agent-acp")
|
|
849
|
-
.option("--worker-adapter <id>", "Adapter for task-work agent (overrides --adapter)")
|
|
850
|
-
.option("--reviewer-adapter <id>", "Adapter for review subagent (overrides --adapter)")
|
|
851
|
-
.option("--adapter-cmd <cmd>", "Custom adapter command (for testing)")
|
|
852
|
-
.option("--restart-every <n>", "Restart agent every N iterations to prevent OOM (0 = never)", "10")
|
|
853
|
-
.option("--focus <instructions>", "Focus instructions included in every iteration prompt")
|
|
854
|
-
.option("--max-tasks <n>", "Max tasks per iteration (0 = unlimited)", "1")
|
|
855
|
-
.option("--tasks <refs>", "Explicit task scope: only work on these tasks (comma-separated refs, e.g., @task1,@task2)")
|
|
856
|
-
.action(async (args, options) => {
|
|
857
|
-
// Check for unknown subcommands that fell through to default
|
|
858
|
-
// Only check args that look like subcommand names (alphanumeric with hyphens, no quotes)
|
|
859
|
-
if (args.length > 0) {
|
|
860
|
-
const unknownCmd = args[0];
|
|
861
|
-
// Skip if it looks like a malformed option or quoted argument
|
|
862
|
-
const looksLikeSubcommand = /^[a-z][a-z0-9-]*$/i.test(unknownCmd);
|
|
863
|
-
if (looksLikeSubcommand) {
|
|
864
|
-
if (unknownCmd === "end-iteration") {
|
|
865
|
-
error(`Unknown command: ${unknownCmd}. Did you mean 'end-loop'?`);
|
|
866
|
-
info("The command was renamed from 'end-iteration' to 'end-loop' to clarify it ends the entire loop.");
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
error(`Unknown command: ${unknownCmd}`);
|
|
870
|
-
}
|
|
871
|
-
info("Run 'kspec ralph --help' to see available commands.");
|
|
872
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
try {
|
|
876
|
-
const maxLoops = parseInt(options.maxLoops, 10);
|
|
877
|
-
const maxRetries = parseInt(options.maxRetries, 10);
|
|
878
|
-
const maxFailures = parseInt(options.maxFailures, 10);
|
|
879
|
-
if (Number.isNaN(maxLoops) || maxLoops < 1) {
|
|
880
|
-
error(errors.usage.maxLoopsPositive);
|
|
881
|
-
process.exit(EXIT_CODES.ERROR);
|
|
882
|
-
}
|
|
883
|
-
if (Number.isNaN(maxRetries) || maxRetries < 0) {
|
|
884
|
-
error(errors.usage.maxRetriesNonNegative);
|
|
885
|
-
process.exit(EXIT_CODES.ERROR);
|
|
886
|
-
}
|
|
887
|
-
if (Number.isNaN(maxFailures) || maxFailures < 1) {
|
|
888
|
-
error(errors.usage.maxFailuresPositive);
|
|
889
|
-
process.exit(EXIT_CODES.ERROR);
|
|
890
|
-
}
|
|
891
|
-
const subagentTimeout = parseInt(options.subagentTimeout, 10);
|
|
892
|
-
if (Number.isNaN(subagentTimeout) || subagentTimeout < 1) {
|
|
893
|
-
error("--subagent-timeout must be a positive integer (minutes)");
|
|
894
|
-
process.exit(EXIT_CODES.ERROR);
|
|
895
|
-
}
|
|
896
|
-
const restartEvery = parseInt(options.restartEvery, 10);
|
|
897
|
-
if (Number.isNaN(restartEvery) || restartEvery < 0) {
|
|
898
|
-
error("--restart-every must be a non-negative integer");
|
|
899
|
-
process.exit(EXIT_CODES.ERROR);
|
|
900
|
-
}
|
|
901
|
-
// AC: @ralph-session-budget-integration ac-create-budget
|
|
902
|
-
const maxTasks = parseInt(options.maxTasks, 10);
|
|
903
|
-
if (Number.isNaN(maxTasks) || maxTasks < 0 || maxTasks > 999) {
|
|
904
|
-
error("--max-tasks must be 0 (unlimited) or a positive integer up to 999");
|
|
905
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
906
|
-
}
|
|
907
|
-
// Handle custom adapter command for testing
|
|
908
|
-
if (options.adapterCmd) {
|
|
909
|
-
const parts = options.adapterCmd.split(/\s+/);
|
|
910
|
-
const customAdapter = {
|
|
911
|
-
command: parts[0],
|
|
912
|
-
args: parts.slice(1),
|
|
913
|
-
description: "Custom adapter via --adapter-cmd",
|
|
914
|
-
};
|
|
915
|
-
registerAdapter("custom", customAdapter);
|
|
916
|
-
options.adapter = "custom";
|
|
917
|
-
}
|
|
918
|
-
// AC: @ralph-per-role-adapters ac-3, ac-4, ac-5
|
|
919
|
-
// Resolve per-role adapters with precedence: role flag > --adapter > default
|
|
920
|
-
const workerAdapterId = options.workerAdapter ?? options.adapter;
|
|
921
|
-
const reviewerAdapterId = options.reviewerAdapter ?? options.adapter;
|
|
922
|
-
const workerAdapter = resolveAdapter(workerAdapterId);
|
|
923
|
-
const reviewerAdapter = resolveAdapter(reviewerAdapterId);
|
|
924
|
-
// AC: @ralph-per-role-adapters ac-6, ac-9, ac-11
|
|
925
|
-
// Validate adapter packages — deduplicate when same ID
|
|
926
|
-
const adapterIdsToValidate = new Set([workerAdapterId, reviewerAdapterId]);
|
|
927
|
-
for (const id of adapterIdsToValidate) {
|
|
928
|
-
const resolved = resolveAdapter(id);
|
|
929
|
-
const isDefault = id === "claude-agent-acp" || id === "claude-code-acp";
|
|
930
|
-
const skip = resolved.command !== "npx" ||
|
|
931
|
-
!resolved.args[0] ||
|
|
932
|
-
(options.dryRun && isDefault);
|
|
933
|
-
if (!skip) {
|
|
934
|
-
validateAdapter(resolved.args[0], id);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
// Build auto-approve extra args per adapter (applied per-spawn to prevent cross-role leakage)
|
|
938
|
-
const workerAutoApproveArgs = options.yolo
|
|
939
|
-
? workerAdapter.autoApproveArgs
|
|
940
|
-
: undefined;
|
|
941
|
-
const reviewerAutoApproveArgs = options.yolo
|
|
942
|
-
? reviewerAdapter.autoApproveArgs
|
|
943
|
-
: undefined;
|
|
944
|
-
const restartInfo = restartEvery > 0 ? `, restart every ${restartEvery}` : "";
|
|
945
|
-
const maxTasksInfo = maxTasks === 0 ? "unlimited" : `${maxTasks}`;
|
|
946
|
-
// Initialize kspec context early to validate --tasks
|
|
947
|
-
const ctx = await initContext();
|
|
948
|
-
// AC: @cli-ralph ac-21 - Parse explicit task scope
|
|
949
|
-
let explicitTaskScope;
|
|
950
|
-
if (options.tasks) {
|
|
951
|
-
try {
|
|
952
|
-
explicitTaskScope = await parseExplicitTasks(ctx, options.tasks);
|
|
953
|
-
info(`Explicit task scope: ${explicitTaskScope.refs.join(", ")}`);
|
|
954
|
-
}
|
|
955
|
-
catch (err) {
|
|
956
|
-
error(`Invalid --tasks argument: ${err.message}`);
|
|
957
|
-
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
const skillOrigins = await loadSkillOriginsForRalph(ctx);
|
|
961
|
-
const workerPromptPlatform = getPromptPlatformForAdapter(workerAdapterId);
|
|
962
|
-
const reviewerPromptPlatform = getPromptPlatformForAdapter(reviewerAdapterId);
|
|
963
|
-
const workerTaskWorkSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.task_work, workerPromptPlatform, skillOrigins);
|
|
964
|
-
const workerReflectSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.reflect, workerPromptPlatform, skillOrigins);
|
|
965
|
-
const reviewerPrReviewSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.pr_review, reviewerPromptPlatform, skillOrigins);
|
|
966
|
-
const taskScopeInfo = explicitTaskScope
|
|
967
|
-
? `, tasks=${explicitTaskScope.refs.join(",")}`
|
|
968
|
-
: "";
|
|
969
|
-
const adapterInfo = workerAdapterId === reviewerAdapterId
|
|
970
|
-
? `adapter=${workerAdapterId}`
|
|
971
|
-
: `worker=${workerAdapterId}, reviewer=${reviewerAdapterId}`;
|
|
972
|
-
info(`Starting ralph loop (${adapterInfo}, max ${maxLoops} iterations, ${maxRetries} retries, ${maxFailures} max failures${restartInfo}, max-tasks=${maxTasksInfo}${taskScopeInfo})`);
|
|
973
|
-
if (options.focus) {
|
|
974
|
-
info(`Focus: ${options.focus}`);
|
|
975
|
-
}
|
|
976
|
-
const specDir = ctx.specDir;
|
|
977
|
-
// Create session for event tracking
|
|
978
|
-
const sessionId = ulid();
|
|
979
|
-
// Set session env vars on this process so all spawned agents
|
|
980
|
-
// (main worker, subagent, wrap-up) inherit them via process.env.
|
|
981
|
-
// KSPEC_RALPH_SESSION: Used by codex skill safety guard to detect ralph context.
|
|
982
|
-
// KSPEC_SESSION_ID: Used by kspec task start for budget enforcement.
|
|
983
|
-
// AC: @ralph-session-budget-integration ac-env-inject
|
|
984
|
-
process.env.KSPEC_RALPH_SESSION = sessionId;
|
|
985
|
-
process.env.KSPEC_SESSION_ID = sessionId;
|
|
986
|
-
// AC: @ralph-session-budget-integration ac-create-budget
|
|
987
|
-
// Create session with budget. When maxTasks=0 (unlimited), no budget.json is created.
|
|
988
|
-
await createSessionWithBudget(specDir, {
|
|
989
|
-
id: sessionId,
|
|
990
|
-
agent_type: workerAdapterId,
|
|
991
|
-
budget: maxTasks,
|
|
992
|
-
});
|
|
993
|
-
// AC: @ralph-per-role-adapters ac-6, ac-7
|
|
994
|
-
// Adapter IDs for harness-specific env injection/cleanup.
|
|
995
|
-
// Deduplicate by harness target, not just adapter ID. claude-code-acp is
|
|
996
|
-
// an alias for claude-agent-acp — both inject to the same Claude Code
|
|
997
|
-
// settings file. Without normalization, injecting twice would clobber the
|
|
998
|
-
// previousValue and break cleanup restoration.
|
|
999
|
-
const normalizeForEnv = (id) => id === "claude-code-acp" ? "claude-agent-acp" : id;
|
|
1000
|
-
const uniqueAdapterIds = [...new Set([
|
|
1001
|
-
normalizeForEnv(workerAdapterId),
|
|
1002
|
-
normalizeForEnv(reviewerAdapterId),
|
|
1003
|
-
])];
|
|
1004
|
-
// Everything after session creation is wrapped in try/finally to guarantee
|
|
1005
|
-
// budget cleanup even if pre-loop setup (event logging, signal handlers) throws.
|
|
1006
|
-
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1007
|
-
let consecutiveFailures = 0;
|
|
1008
|
-
let agent = null;
|
|
1009
|
-
let acpSessionId = null;
|
|
1010
|
-
let exitReason = null;
|
|
1011
|
-
let lastIterationCtx = null;
|
|
1012
|
-
let lastErrorMessage;
|
|
1013
|
-
let latestIteration = 0;
|
|
1014
|
-
// AC: @ralph-per-role-adapters ac-7
|
|
1015
|
-
// Track previous env values per adapter for cleanup restoration
|
|
1016
|
-
const previousEnvValues = new Map();
|
|
1017
|
-
const recentTaskRefs = [];
|
|
1018
|
-
const sessionIterationMap = new Map();
|
|
1019
|
-
const endAcpSession = (spawned, sessionToEnd) => {
|
|
1020
|
-
if (!spawned || !sessionToEnd) {
|
|
1021
|
-
return null;
|
|
1022
|
-
}
|
|
1023
|
-
spawned.client.endSession(sessionToEnd);
|
|
1024
|
-
sessionIterationMap.delete(sessionToEnd);
|
|
1025
|
-
return null;
|
|
1026
|
-
};
|
|
1027
|
-
// Signal handler refs — declared here so finally can remove them
|
|
1028
|
-
// AC: @ralph-task-limit ac-signal-cleanup
|
|
1029
|
-
const signalCleanup = (signal) => {
|
|
1030
|
-
info(`Received ${signal}, cleaning up...`);
|
|
1031
|
-
if (agent) {
|
|
1032
|
-
acpSessionId = endAcpSession(agent, acpSessionId);
|
|
1033
|
-
agent = disposeSpawnedAgent(agent);
|
|
1034
|
-
}
|
|
1035
|
-
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1036
|
-
// Must use async IIFE — signal handlers are called synchronously,
|
|
1037
|
-
// but cleanup needs async I/O. The IIFE keeps the event loop alive
|
|
1038
|
-
// until cleanup completes, then exits explicitly.
|
|
1039
|
-
void (async () => {
|
|
1040
|
-
try {
|
|
1041
|
-
await Promise.all([
|
|
1042
|
-
fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { }),
|
|
1043
|
-
closeSession(specDir, sessionId, "abandoned", `Received ${signal}`),
|
|
1044
|
-
...uniqueAdapterIds.map((id) => removeEnvForAdapter(id, previousEnvValues.get(id))),
|
|
1045
|
-
]);
|
|
1046
|
-
}
|
|
1047
|
-
catch {
|
|
1048
|
-
// Best-effort cleanup — don't let errors prevent exit
|
|
1049
|
-
}
|
|
1050
|
-
finally {
|
|
1051
|
-
process.exit(0);
|
|
1052
|
-
}
|
|
1053
|
-
})();
|
|
1054
|
-
};
|
|
1055
|
-
const sigintHandler = () => { signalCleanup("SIGINT"); };
|
|
1056
|
-
const sigtermHandler = () => { signalCleanup("SIGTERM"); };
|
|
1057
|
-
try {
|
|
1058
|
-
// AC: @session-end-loop-signal ac-session-close-signal
|
|
1059
|
-
// Install signal handlers FIRST, before any async work, so signals
|
|
1060
|
-
// during startup (e.g. during appendEvent) still trigger cleanup.
|
|
1061
|
-
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1062
|
-
process.on("SIGINT", sigintHandler);
|
|
1063
|
-
process.on("SIGTERM", sigtermHandler);
|
|
1064
|
-
// AC: @ralph-per-role-adapters ac-6, ac-7
|
|
1065
|
-
// Inject KSPEC_SESSION_ID into agent harness config for each unique adapter.
|
|
1066
|
-
// Process env alone is insufficient — some harnesses (e.g., Claude Code)
|
|
1067
|
-
// sandbox child processes and don't forward arbitrary parent env vars.
|
|
1068
|
-
// AC: @ralph-session-budget-integration ac-env-inject
|
|
1069
|
-
for (const id of uniqueAdapterIds) {
|
|
1070
|
-
const injectionResult = await injectEnvForAdapter(id, sessionId);
|
|
1071
|
-
previousEnvValues.set(id, injectionResult?.previousValue);
|
|
1072
|
-
}
|
|
1073
|
-
// AC: @ralph-per-role-adapters ac-12
|
|
1074
|
-
// Log session start with both adapter IDs
|
|
1075
|
-
await appendEvent(specDir, {
|
|
1076
|
-
session_id: sessionId,
|
|
1077
|
-
type: "session.start",
|
|
1078
|
-
data: {
|
|
1079
|
-
adapter: workerAdapterId,
|
|
1080
|
-
workerAdapter: workerAdapterId,
|
|
1081
|
-
reviewerAdapter: reviewerAdapterId,
|
|
1082
|
-
maxLoops,
|
|
1083
|
-
maxRetries,
|
|
1084
|
-
maxFailures,
|
|
1085
|
-
maxTasks,
|
|
1086
|
-
yolo: options.yolo,
|
|
1087
|
-
focus: options.focus,
|
|
1088
|
-
explicitTasks: explicitTaskScope?.refs,
|
|
1089
|
-
},
|
|
1090
|
-
});
|
|
1091
|
-
// Create translator and renderer for this session
|
|
1092
|
-
const translator = createTranslator();
|
|
1093
|
-
const renderer = createCliRenderer();
|
|
1094
|
-
for (let iteration = 1; iteration <= maxLoops; iteration++) {
|
|
1095
|
-
latestIteration = iteration;
|
|
1096
|
-
renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
|
|
1097
|
-
// AC: @ralph-session-budget-integration ac-reset-iteration
|
|
1098
|
-
// Reset budget counter at iteration start (no-op when no budget exists)
|
|
1099
|
-
await resetBudget(specDir, sessionId);
|
|
1100
|
-
// AC: @session-end-loop-signal ac-detect - Check session state for end-loop
|
|
1101
|
-
const endLoopState = await isEndLoopRequested(specDir, sessionId);
|
|
1102
|
-
if (endLoopState?.requested) {
|
|
1103
|
-
info(`End-loop already requested for this session. Exiting.`);
|
|
1104
|
-
exitReason = "end_loop_signal";
|
|
1105
|
-
break;
|
|
1106
|
-
}
|
|
1107
|
-
// Gather fresh context each iteration
|
|
1108
|
-
// AC: @cli-ralph ac-16 - Only automation-eligible tasks (unless explicit scope)
|
|
1109
|
-
// AC: @cli-ralph ac-21 - With explicit task scope, ignore automation eligibility
|
|
1110
|
-
let sessionCtx = await gatherSessionContext(ctx, {
|
|
1111
|
-
limit: "10",
|
|
1112
|
-
eligible: !explicitTaskScope, // Skip eligibility filter if explicit scope
|
|
1113
|
-
});
|
|
1114
|
-
// AC: @cli-ralph ac-21 - Filter to explicit tasks if scope is set
|
|
1115
|
-
if (explicitTaskScope) {
|
|
1116
|
-
sessionCtx = filterByExplicitTasks(sessionCtx, explicitTaskScope);
|
|
1117
|
-
}
|
|
1118
|
-
// AC: @ralph-subagent-spawning ac-8 - Process pending_review tasks BEFORE main iteration
|
|
1119
|
-
// AC: @ralph-per-role-adapters ac-2 - Use reviewer adapter for review subagents
|
|
1120
|
-
// This wraps consecutiveFailures in an object so it can be mutated by the helper
|
|
1121
|
-
const failureTracker = { count: consecutiveFailures };
|
|
1122
|
-
const continueLoop = await processPendingReviewTasks(ctx, reviewerAdapter, sessionCtx.pending_review_tasks, {
|
|
1123
|
-
yolo: options.yolo,
|
|
1124
|
-
maxRetries,
|
|
1125
|
-
maxFailures,
|
|
1126
|
-
cwd: process.cwd(),
|
|
1127
|
-
specDir,
|
|
1128
|
-
sessionId,
|
|
1129
|
-
subagentTimeout: subagentTimeout * 60 * 1000,
|
|
1130
|
-
autoApproveArgs: reviewerAutoApproveArgs,
|
|
1131
|
-
prReviewSkillName: reviewerPrReviewSkill,
|
|
1132
|
-
}, failureTracker);
|
|
1133
|
-
consecutiveFailures = failureTracker.count;
|
|
1134
|
-
if (!continueLoop) {
|
|
1135
|
-
exitReason = "max_failures";
|
|
1136
|
-
lastIterationCtx = sessionCtx;
|
|
1137
|
-
break;
|
|
1138
|
-
}
|
|
1139
|
-
// AC: @cli-ralph ac-20 - Refresh context after pending_review processing
|
|
1140
|
-
// If pending_review tasks were processed, they may have completed and unblocked
|
|
1141
|
-
// dependent tasks. Re-gather context to detect newly available tasks.
|
|
1142
|
-
let currentCtx = sessionCtx;
|
|
1143
|
-
if (sessionCtx.pending_review_tasks.length > 0) {
|
|
1144
|
-
currentCtx = await gatherSessionContext(ctx, {
|
|
1145
|
-
limit: "10",
|
|
1146
|
-
eligible: !explicitTaskScope,
|
|
1147
|
-
});
|
|
1148
|
-
if (explicitTaskScope) {
|
|
1149
|
-
currentCtx = filterByExplicitTasks(currentCtx, explicitTaskScope);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
// AC: @cli-ralph ac-21 - Check explicit task completion
|
|
1153
|
-
if (explicitTaskScope) {
|
|
1154
|
-
const { done, statuses } = await allExplicitTasksDone(ctx, explicitTaskScope);
|
|
1155
|
-
if (done) {
|
|
1156
|
-
const statusList = Array.from(statuses.entries())
|
|
1157
|
-
.map(([ref, status]) => `${ref}: ${status}`)
|
|
1158
|
-
.join(", ");
|
|
1159
|
-
info(`All explicit tasks completed or blocked (${statusList}). Exiting loop.`);
|
|
1160
|
-
exitReason = "explicit_tasks_done";
|
|
1161
|
-
lastIterationCtx = currentCtx;
|
|
1162
|
-
break;
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
// Check for automation-eligible tasks (ready or in_progress)
|
|
1166
|
-
// AC: @cli-ralph ac-19
|
|
1167
|
-
const hasActiveTasks = currentCtx.active_tasks.length > 0;
|
|
1168
|
-
const hasReadyTasks = currentCtx.ready_tasks.length > 0;
|
|
1169
|
-
if (!hasActiveTasks && !hasReadyTasks) {
|
|
1170
|
-
if (explicitTaskScope) {
|
|
1171
|
-
info("No explicit tasks available (ready or in_progress). Exiting loop.");
|
|
1172
|
-
}
|
|
1173
|
-
else {
|
|
1174
|
-
info("No automation-eligible tasks (ready or in_progress). Exiting loop.");
|
|
1175
|
-
}
|
|
1176
|
-
exitReason = "no_tasks";
|
|
1177
|
-
lastIterationCtx = currentCtx;
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
1180
|
-
// AC: @loop-mode-error-handling - Track tasks in progress for failure handling
|
|
1181
|
-
const tasksInProgressAtStart = sessionCtx.active_tasks;
|
|
1182
|
-
const iterationStartTime = new Date();
|
|
1183
|
-
// Build prompts - task-work first, then reflect
|
|
1184
|
-
// AC: @cli-ralph ac-21 - Include explicit task scope in prompt
|
|
1185
|
-
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, workerTaskWorkSkill, options.focus, explicitTaskScope);
|
|
1186
|
-
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId, workerReflectSkill);
|
|
1187
|
-
// AC: @cli-ralph ac-21
|
|
1188
|
-
// AC: @ralph-per-role-adapters ac-10
|
|
1189
|
-
if (options.dryRun) {
|
|
1190
|
-
console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
|
|
1191
|
-
console.log(` worker-adapter: ${workerAdapterId}`);
|
|
1192
|
-
console.log(` reviewer-adapter: ${reviewerAdapterId}`);
|
|
1193
|
-
console.log(` max-loops: ${maxLoops}`);
|
|
1194
|
-
console.log(` max-tasks: ${maxTasks === 0 ? "unlimited" : maxTasks}`);
|
|
1195
|
-
console.log(` max-retries: ${maxRetries}`);
|
|
1196
|
-
console.log(` max-failures: ${maxFailures}`);
|
|
1197
|
-
console.log(` restart-every: ${restartEvery === 0 ? "never" : restartEvery}`);
|
|
1198
|
-
console.log(` worker-task-work-skill: ${workerTaskWorkSkill}`);
|
|
1199
|
-
console.log(` worker-reflect-skill: ${workerReflectSkill}`);
|
|
1200
|
-
console.log(` reviewer-pr-review-skill: ${reviewerPrReviewSkill}`);
|
|
1201
|
-
if (explicitTaskScope) {
|
|
1202
|
-
console.log(` explicit-tasks: ${explicitTaskScope.refs.join(", ")}`);
|
|
1203
|
-
}
|
|
1204
|
-
console.log(chalk.yellow("\n=== Task Work Prompt ===\n"));
|
|
1205
|
-
console.log(taskWorkPrompt);
|
|
1206
|
-
console.log(chalk.yellow("\n=== Reflect Prompt ===\n"));
|
|
1207
|
-
console.log(reflectPrompt);
|
|
1208
|
-
console.log(chalk.yellow("\n=== END DRY RUN ==="));
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
// Log task-work prompt
|
|
1212
|
-
await appendEvent(specDir, {
|
|
1213
|
-
session_id: sessionId,
|
|
1214
|
-
type: "prompt.sent",
|
|
1215
|
-
data: {
|
|
1216
|
-
iteration,
|
|
1217
|
-
phase: "task-work",
|
|
1218
|
-
prompt: taskWorkPrompt,
|
|
1219
|
-
tasks: {
|
|
1220
|
-
active: currentCtx.active_tasks.map((t) => t.ref),
|
|
1221
|
-
ready: currentCtx.ready_tasks.map((t) => t.ref),
|
|
1222
|
-
},
|
|
1223
|
-
},
|
|
1224
|
-
});
|
|
1225
|
-
// Retry loop for this iteration
|
|
1226
|
-
let lastError = null;
|
|
1227
|
-
let succeeded = false;
|
|
1228
|
-
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
1229
|
-
if (attempt > 1) {
|
|
1230
|
-
console.log(chalk.yellow(`\nRetry attempt ${attempt - 1}/${maxRetries}...`));
|
|
1231
|
-
}
|
|
1232
|
-
try {
|
|
1233
|
-
// Spawn agent if not already running
|
|
1234
|
-
// AC: @ralph-per-role-adapters ac-1 - Use worker adapter for task-work
|
|
1235
|
-
if (!agent) {
|
|
1236
|
-
info("Spawning ACP agent...");
|
|
1237
|
-
// AC: @ralph-session-budget-integration ac-env-inject
|
|
1238
|
-
// AC: @ralph-adapter-auto-approve ac-1, ac-2, ac-3
|
|
1239
|
-
agent = await spawnAndInitialize(workerAdapter, {
|
|
1240
|
-
cwd: process.cwd(),
|
|
1241
|
-
env: { KSPEC_SESSION_ID: sessionId },
|
|
1242
|
-
extraArgs: workerAutoApproveArgs,
|
|
1243
|
-
clientOptions: {
|
|
1244
|
-
clientInfo: {
|
|
1245
|
-
name: "kspec-ralph",
|
|
1246
|
-
version: packageVersion,
|
|
1247
|
-
},
|
|
1248
|
-
methodTimeouts: {
|
|
1249
|
-
"session/prompt": RALPH_PROMPT_TIMEOUT,
|
|
1250
|
-
"session/resume": RALPH_PROMPT_TIMEOUT,
|
|
1251
|
-
},
|
|
1252
|
-
},
|
|
1253
|
-
});
|
|
1254
|
-
// Set up streaming update handler with translator + renderer
|
|
1255
|
-
agent.client.on("update", (_sid, update) => {
|
|
1256
|
-
// Translate ACP event to RalphEvent and render
|
|
1257
|
-
const event = translator.translate(update);
|
|
1258
|
-
if (event) {
|
|
1259
|
-
renderer.render(event);
|
|
1260
|
-
}
|
|
1261
|
-
// Log raw update event (async, non-blocking)
|
|
1262
|
-
// Look up iteration by ACP session ID so late updates from
|
|
1263
|
-
// a previous session are attributed to the correct iteration
|
|
1264
|
-
const eventIteration = sessionIterationMap.get(_sid) ?? latestIteration;
|
|
1265
|
-
appendEvent(specDir, {
|
|
1266
|
-
session_id: sessionId,
|
|
1267
|
-
type: "session.update",
|
|
1268
|
-
data: { iteration: eventIteration, update },
|
|
1269
|
-
}).catch(() => {
|
|
1270
|
-
// Ignore logging errors during streaming
|
|
1271
|
-
});
|
|
1272
|
-
});
|
|
1273
|
-
// Set up tool request handler
|
|
1274
|
-
agent.client.on("request", (reqId, method, params) => {
|
|
1275
|
-
// biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
|
|
1276
|
-
handleRequest(agent.client, reqId, method, params, {
|
|
1277
|
-
yolo: options.yolo,
|
|
1278
|
-
specDir,
|
|
1279
|
-
sessionId,
|
|
1280
|
-
}).catch((err) => {
|
|
1281
|
-
// biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
|
|
1282
|
-
agent.client.respondError(reqId, -32000, err.message);
|
|
1283
|
-
});
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
// Create fresh ACP session per iteration to keep context clean
|
|
1287
|
-
info("Creating ACP session...");
|
|
1288
|
-
acpSessionId = await agent.client.newSession({
|
|
1289
|
-
cwd: process.cwd(),
|
|
1290
|
-
mcpServers: [], // No MCP servers for now
|
|
1291
|
-
});
|
|
1292
|
-
setSessionIteration(sessionIterationMap, acpSessionId, iteration);
|
|
1293
|
-
// Phase 1: Task Work
|
|
1294
|
-
info("Sending task-work prompt to agent...");
|
|
1295
|
-
const taskWorkResponse = await agent.client.prompt({
|
|
1296
|
-
sessionId: acpSessionId,
|
|
1297
|
-
prompt: [{ type: "text", text: taskWorkPrompt }],
|
|
1298
|
-
});
|
|
1299
|
-
// Log task-work completion
|
|
1300
|
-
await appendEvent(specDir, {
|
|
1301
|
-
session_id: sessionId,
|
|
1302
|
-
type: "session.update",
|
|
1303
|
-
data: {
|
|
1304
|
-
iteration,
|
|
1305
|
-
phase: "task-work",
|
|
1306
|
-
stopReason: taskWorkResponse.stopReason,
|
|
1307
|
-
completed: true,
|
|
1308
|
-
},
|
|
1309
|
-
});
|
|
1310
|
-
if (taskWorkResponse.stopReason === "cancelled") {
|
|
1311
|
-
throw new Error(errors.usage.agentPromptCancelled);
|
|
1312
|
-
}
|
|
1313
|
-
// Phase 2: Reflect (always sent after task-work completes)
|
|
1314
|
-
info("Sending reflect prompt to agent...");
|
|
1315
|
-
await appendEvent(specDir, {
|
|
1316
|
-
session_id: sessionId,
|
|
1317
|
-
type: "prompt.sent",
|
|
1318
|
-
data: {
|
|
1319
|
-
iteration,
|
|
1320
|
-
phase: "reflect",
|
|
1321
|
-
prompt: reflectPrompt,
|
|
1322
|
-
},
|
|
1323
|
-
});
|
|
1324
|
-
const reflectResponse = await agent.client.prompt({
|
|
1325
|
-
sessionId: acpSessionId,
|
|
1326
|
-
prompt: [{ type: "text", text: reflectPrompt }],
|
|
1327
|
-
});
|
|
1328
|
-
// Log reflect completion
|
|
1329
|
-
await appendEvent(specDir, {
|
|
1330
|
-
session_id: sessionId,
|
|
1331
|
-
type: "session.update",
|
|
1332
|
-
data: {
|
|
1333
|
-
iteration,
|
|
1334
|
-
phase: "reflect",
|
|
1335
|
-
stopReason: reflectResponse.stopReason,
|
|
1336
|
-
completed: true,
|
|
1337
|
-
},
|
|
1338
|
-
});
|
|
1339
|
-
if (reflectResponse.stopReason === "cancelled") {
|
|
1340
|
-
throw new Error(errors.usage.agentPromptCancelled);
|
|
1341
|
-
}
|
|
1342
|
-
acpSessionId = endAcpSession(agent, acpSessionId);
|
|
1343
|
-
succeeded = true;
|
|
1344
|
-
break;
|
|
1345
|
-
}
|
|
1346
|
-
catch (err) {
|
|
1347
|
-
lastError = err;
|
|
1348
|
-
error(errors.failures.iterationFailed(lastError.message));
|
|
1349
|
-
// Clean up agent on error - will respawn next attempt
|
|
1350
|
-
if (agent) {
|
|
1351
|
-
acpSessionId = endAcpSession(agent, acpSessionId);
|
|
1352
|
-
agent = disposeSpawnedAgent(agent);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
if (succeeded) {
|
|
1357
|
-
console.log(); // Newline after streaming output
|
|
1358
|
-
// Save session context snapshot for audit trail
|
|
1359
|
-
await saveSessionContext(specDir, sessionId, iteration, sessionCtx);
|
|
1360
|
-
success(`Completed iteration ${iteration}`);
|
|
1361
|
-
consecutiveFailures = 0;
|
|
1362
|
-
// Track task refs from this iteration for wrap-up context
|
|
1363
|
-
for (const t of sessionCtx.active_tasks) {
|
|
1364
|
-
pushRecentTaskRef(recentTaskRefs, t.ref);
|
|
1365
|
-
}
|
|
1366
|
-
lastIterationCtx = sessionCtx;
|
|
1367
|
-
// Periodic agent restart to prevent OOM
|
|
1368
|
-
// AC: @cli-ralph ac-restart-periodic
|
|
1369
|
-
if (restartEvery > 0 &&
|
|
1370
|
-
iteration % restartEvery === 0 &&
|
|
1371
|
-
iteration < maxLoops) {
|
|
1372
|
-
info(`Restarting agent to prevent memory buildup (every ${restartEvery} iterations)...`);
|
|
1373
|
-
if (agent) {
|
|
1374
|
-
acpSessionId = endAcpSession(agent, acpSessionId);
|
|
1375
|
-
agent = disposeSpawnedAgent(agent);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
else {
|
|
1380
|
-
consecutiveFailures++;
|
|
1381
|
-
error(errors.failures.iterationFailedAfterRetries(iteration, maxRetries, consecutiveFailures, maxFailures));
|
|
1382
|
-
if (lastError) {
|
|
1383
|
-
error(errors.failures.lastError(lastError.message));
|
|
1384
|
-
}
|
|
1385
|
-
// AC: @loop-mode-error-handling - Track per-task failures
|
|
1386
|
-
const errorDesc = lastError?.message || "Iteration failed after retries";
|
|
1387
|
-
await handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDesc);
|
|
1388
|
-
if (consecutiveFailures >= maxFailures) {
|
|
1389
|
-
error(errors.failures.reachedMaxFailures(maxFailures));
|
|
1390
|
-
exitReason = "max_failures";
|
|
1391
|
-
lastErrorMessage = lastError?.message;
|
|
1392
|
-
lastIterationCtx = sessionCtx;
|
|
1393
|
-
break;
|
|
1394
|
-
}
|
|
1395
|
-
info("Continuing to next iteration...");
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
// If loop completed all iterations without breaking
|
|
1399
|
-
if (exitReason === null) {
|
|
1400
|
-
exitReason = "max_iterations";
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
catch (loopErr) {
|
|
1404
|
-
// AC: @session-end-loop-signal ac-session-close-error
|
|
1405
|
-
// Unrecoverable error during loop execution
|
|
1406
|
-
exitReason = exitReason ?? "error";
|
|
1407
|
-
lastErrorMessage = loopErr.message;
|
|
1408
|
-
error("Unrecoverable error in ralph loop", loopErr);
|
|
1409
|
-
}
|
|
1410
|
-
finally {
|
|
1411
|
-
// Remove signal handlers to avoid double cleanup
|
|
1412
|
-
process.off("SIGINT", sigintHandler);
|
|
1413
|
-
process.off("SIGTERM", sigtermHandler);
|
|
1414
|
-
// Clean up agent
|
|
1415
|
-
if (agent) {
|
|
1416
|
-
acpSessionId = endAcpSession(agent, acpSessionId);
|
|
1417
|
-
agent = disposeSpawnedAgent(agent);
|
|
1418
|
-
}
|
|
1419
|
-
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1420
|
-
// AC: @ralph-per-role-adapters ac-7 - Clean up env for all unique adapters
|
|
1421
|
-
await fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { });
|
|
1422
|
-
for (const id of uniqueAdapterIds) {
|
|
1423
|
-
await removeEnvForAdapter(id, previousEnvValues.get(id));
|
|
1424
|
-
}
|
|
1425
|
-
// Clean up session env vars
|
|
1426
|
-
delete process.env.KSPEC_RALPH_SESSION;
|
|
1427
|
-
delete process.env.KSPEC_SESSION_ID;
|
|
1428
|
-
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1, ac-2, ac-3, ac-4, ac-5
|
|
1429
|
-
// Spawn wrap-up agent if not dry-run and we have an exit reason
|
|
1430
|
-
if (!options.dryRun && exitReason) {
|
|
1431
|
-
console.log("");
|
|
1432
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1433
|
-
console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Starting Wrap-Up`));
|
|
1434
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1435
|
-
console.log("");
|
|
1436
|
-
const inProgressTasks = lastIterationCtx?.active_tasks || [];
|
|
1437
|
-
const pendingReviewTasks = lastIterationCtx?.pending_review_tasks || [];
|
|
1438
|
-
const wrapUpCtx = buildWrapUpContext(exitReason, sessionId, maxLoops, // Use maxLoops as iteration (we're at the end)
|
|
1439
|
-
maxLoops, inProgressTasks, pendingReviewTasks, recentTaskRefs, process.cwd(), lastErrorMessage);
|
|
1440
|
-
info(`Exit reason: ${exitReason}`);
|
|
1441
|
-
info(`Working tree: ${wrapUpCtx.workingTree.clean ? "clean" : "has uncommitted changes"}`);
|
|
1442
|
-
// AC: @ralph-per-role-adapters ac-8 - Wrap-up uses worker adapter
|
|
1443
|
-
const wrapUpResult = await runWrapUpAgent(workerAdapter, wrapUpCtx, {
|
|
1444
|
-
yolo: options.yolo,
|
|
1445
|
-
cwd: process.cwd(),
|
|
1446
|
-
extraArgs: workerAutoApproveArgs,
|
|
1447
|
-
handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, {
|
|
1448
|
-
yolo: options.yolo,
|
|
1449
|
-
specDir,
|
|
1450
|
-
sessionId,
|
|
1451
|
-
}),
|
|
1452
|
-
}, DEFAULT_WRAPUP_TIMEOUT);
|
|
1453
|
-
// Log wrap-up result
|
|
1454
|
-
await appendEvent(specDir, {
|
|
1455
|
-
session_id: sessionId,
|
|
1456
|
-
type: "session.wrapup",
|
|
1457
|
-
data: {
|
|
1458
|
-
exitReason,
|
|
1459
|
-
result: wrapUpResult,
|
|
1460
|
-
},
|
|
1461
|
-
});
|
|
1462
|
-
if (wrapUpResult.skipped) {
|
|
1463
|
-
info(`${WRAPUP_AGENT_PREFIX} Skipped: ${wrapUpResult.skipReason}`);
|
|
1464
|
-
}
|
|
1465
|
-
else if (wrapUpResult.timedOut) {
|
|
1466
|
-
warn(`${WRAPUP_AGENT_PREFIX} Timed out after ${DEFAULT_WRAPUP_TIMEOUT / 1000}s`);
|
|
1467
|
-
}
|
|
1468
|
-
else if (!wrapUpResult.success) {
|
|
1469
|
-
warn(`${WRAPUP_AGENT_PREFIX} Failed: ${wrapUpResult.error}`);
|
|
1470
|
-
}
|
|
1471
|
-
else {
|
|
1472
|
-
success(`${WRAPUP_AGENT_PREFIX} Completed`);
|
|
1473
|
-
}
|
|
1474
|
-
console.log("");
|
|
1475
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1476
|
-
console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Wrap-Up Complete`));
|
|
1477
|
-
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1478
|
-
console.log("");
|
|
1479
|
-
}
|
|
1480
|
-
// Log session end and close session with appropriate status/reason
|
|
1481
|
-
// AC: @session-end-loop-signal ac-session-close-normal, ac-session-close-error
|
|
1482
|
-
const isErrorExit = consecutiveFailures >= maxFailures ||
|
|
1483
|
-
exitReason === "max_failures" ||
|
|
1484
|
-
exitReason === "error";
|
|
1485
|
-
const status = isErrorExit ? "abandoned" : "completed";
|
|
1486
|
-
const closeReason = exitReason === "max_failures"
|
|
1487
|
-
? `Max failures reached (${consecutiveFailures}/${maxFailures})${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
|
|
1488
|
-
: exitReason === "error"
|
|
1489
|
-
? `Unrecoverable error${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
|
|
1490
|
-
: exitReason === "end_loop_signal"
|
|
1491
|
-
? "Agent requested end of loop"
|
|
1492
|
-
: exitReason === "max_iterations"
|
|
1493
|
-
? `Completed all ${maxLoops} iterations`
|
|
1494
|
-
: exitReason === "no_tasks"
|
|
1495
|
-
? "No eligible tasks remaining"
|
|
1496
|
-
: exitReason === "explicit_tasks_done"
|
|
1497
|
-
? "All explicit tasks completed"
|
|
1498
|
-
: `Loop ended: ${exitReason}`;
|
|
1499
|
-
await appendEvent(specDir, {
|
|
1500
|
-
session_id: sessionId,
|
|
1501
|
-
type: "session.end",
|
|
1502
|
-
data: {
|
|
1503
|
-
status,
|
|
1504
|
-
consecutiveFailures,
|
|
1505
|
-
exitReason,
|
|
1506
|
-
closeReason,
|
|
1507
|
-
},
|
|
1508
|
-
});
|
|
1509
|
-
await closeSession(specDir, sessionId, status, closeReason);
|
|
1510
|
-
}
|
|
1511
|
-
console.log(chalk.green(`\n${"─".repeat(60)}`));
|
|
1512
|
-
success("Ralph loop completed");
|
|
1513
|
-
console.log(chalk.green(`${"─".repeat(60)}\n`));
|
|
1514
|
-
}
|
|
1515
|
-
catch (err) {
|
|
1516
|
-
error(errors.failures.ralphLoop, err);
|
|
1517
|
-
process.exit(EXIT_CODES.ERROR);
|
|
1518
|
-
}
|
|
31
|
+
.allowUnknownOption()
|
|
32
|
+
.action(() => {
|
|
33
|
+
showRalphDeprecationError();
|
|
1519
34
|
});
|
|
1520
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Display a migration error message explaining that ralph has been replaced.
|
|
38
|
+
*
|
|
39
|
+
* AC: @ralph-replacement ac-1 — error message lists equivalent commands for
|
|
40
|
+
* common ralph operations (run, end-loop, dry-run)
|
|
41
|
+
* AC: @trait-error-guidance ac-1 — includes description of what went wrong
|
|
42
|
+
* AC: @trait-error-guidance ac-2 — includes suggested action to resolve
|
|
43
|
+
*/
|
|
44
|
+
function showRalphDeprecationError() {
|
|
45
|
+
const header = chalk.red("✗ kspec ralph has been replaced by kspec agent");
|
|
46
|
+
const msg = [
|
|
47
|
+
header,
|
|
48
|
+
"",
|
|
49
|
+
chalk.bold("kspec ralph has been removed.") +
|
|
50
|
+
" Use " +
|
|
51
|
+
chalk.cyan("kspec agent") +
|
|
52
|
+
" for equivalent functionality.",
|
|
53
|
+
"",
|
|
54
|
+
chalk.bold("Equivalent commands:"),
|
|
55
|
+
` ${chalk.yellow("kspec ralph run")} → ${chalk.cyan("kspec agent dispatch start")}`,
|
|
56
|
+
` ${chalk.yellow("kspec ralph --dry-run")} → ${chalk.cyan("kspec agent dispatch start --dry-run")}`,
|
|
57
|
+
` ${chalk.yellow("kspec ralph end-loop")} → ${chalk.cyan("kspec agent end-loop")}`,
|
|
58
|
+
"",
|
|
59
|
+
chalk.bold("Getting started:"),
|
|
60
|
+
` List configured agents: ${chalk.cyan("kspec agent list")}`,
|
|
61
|
+
` Run a specific agent: ${chalk.cyan("kspec agent run <agent-id>")}`,
|
|
62
|
+
` Start dispatch engine: ${chalk.cyan("kspec agent dispatch start")}`,
|
|
63
|
+
` Check dispatch status: ${chalk.cyan("kspec agent dispatch status")}`,
|
|
64
|
+
"",
|
|
65
|
+
`Run ${chalk.cyan("kspec setup")} to create built-in worker and reviewer agent definitions.`,
|
|
66
|
+
`Run ${chalk.cyan("kspec agent --help")} for full documentation.`,
|
|
67
|
+
].join("\n");
|
|
68
|
+
process.stderr.write(msg + "\n");
|
|
69
|
+
process.exit(EXIT_CODES.ERROR);
|
|
70
|
+
}
|
|
1521
71
|
//# sourceMappingURL=ralph.js.map
|