@kynetic-ai/spec 0.3.0 → 0.5.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/cli/batch-exec.d.ts +0 -9
- package/dist/cli/batch-exec.d.ts.map +1 -1
- package/dist/cli/batch-exec.js +16 -4
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +2 -1
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/guard.d.ts +43 -0
- package/dist/cli/commands/guard.d.ts.map +1 -0
- package/dist/cli/commands/guard.js +200 -0
- package/dist/cli/commands/guard.js.map +1 -0
- 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 +18 -0
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +5 -4
- 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 +2 -1
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts.map +1 -1
- package/dist/cli/commands/plan-import.js +100 -30
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +143 -330
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/session.d.ts +73 -1
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +607 -162
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +97 -217
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts +4 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +62 -5
- 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 +128 -59
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.d.ts.map +1 -1
- package/dist/cli/commands/tasks.js +2 -4
- 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 +12 -98
- package/dist/cli/commands/triage.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 +18 -4
- package/dist/cli/output.js.map +1 -1
- package/dist/daemon/routes/triage.ts +4 -70
- package/dist/parser/config.d.ts +106 -0
- package/dist/parser/config.d.ts.map +1 -1
- package/dist/parser/config.js +47 -0
- package/dist/parser/config.js.map +1 -1
- package/dist/parser/file-lock.d.ts +14 -0
- package/dist/parser/file-lock.d.ts.map +1 -0
- package/dist/parser/file-lock.js +124 -0
- package/dist/parser/file-lock.js.map +1 -0
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/plan-document.d.ts +44 -0
- package/dist/parser/plan-document.d.ts.map +1 -1
- package/dist/parser/plan-document.js +76 -8
- package/dist/parser/plan-document.js.map +1 -1
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +28 -102
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +11 -7
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +322 -297
- package/dist/parser/yaml.js.map +1 -1
- package/dist/ralph/events.d.ts.map +1 -1
- package/dist/ralph/events.js +24 -0
- package/dist/ralph/events.js.map +1 -1
- package/dist/ralph/index.d.ts +1 -1
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +1 -1
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/subagent.d.ts +12 -1
- package/dist/ralph/subagent.d.ts.map +1 -1
- package/dist/ralph/subagent.js +22 -3
- package/dist/ralph/subagent.js.map +1 -1
- package/dist/schema/batch.d.ts +2 -0
- package/dist/schema/batch.d.ts.map +1 -1
- package/dist/schema/common.d.ts +6 -0
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +8 -0
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/task.d.ts +22 -0
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +7 -0
- package/dist/schema/task.js.map +1 -1
- package/dist/sessions/store.d.ts +226 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +712 -38
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +51 -2
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +25 -0
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +4 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +2 -0
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/labels.d.ts +2 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +2 -0
- package/dist/strings/labels.js.map +1 -1
- package/dist/triage/actions.d.ts +27 -0
- package/dist/triage/actions.d.ts.map +1 -0
- package/dist/triage/actions.js +95 -0
- package/dist/triage/actions.js.map +1 -0
- package/dist/triage/constants.d.ts +6 -0
- package/dist/triage/constants.d.ts.map +1 -0
- package/dist/triage/constants.js +7 -0
- package/dist/triage/constants.js.map +1 -0
- package/dist/triage/index.d.ts +3 -0
- package/dist/triage/index.d.ts.map +1 -0
- package/dist/triage/index.js +3 -0
- package/dist/triage/index.js.map +1 -0
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +21 -5
- package/dist/utils/git.js.map +1 -1
- package/package.json +1 -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 +235 -0
- package/plugin/plugins/kspec/skills/observations/SKILL.md +143 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +343 -0
- package/plugin/plugins/kspec/skills/reflect/SKILL.md +161 -0
- package/plugin/plugins/kspec/skills/review/SKILL.md +230 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +319 -0
- package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +140 -0
- package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +232 -0
- package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +354 -0
- package/templates/agents-sections/03-task-lifecycle.md +2 -2
- package/templates/agents-sections/04-pr-workflow.md +3 -3
- package/templates/agents-sections/05-commit-convention.md +14 -0
- package/templates/skills/create-workflow/SKILL.md +228 -0
- package/templates/skills/manifest.yaml +45 -0
- package/templates/skills/observations/SKILL.md +137 -0
- package/templates/skills/plan/SKILL.md +336 -0
- package/templates/skills/reflect/SKILL.md +155 -0
- package/templates/skills/review/SKILL.md +223 -0
- package/templates/skills/task-work/SKILL.md +312 -0
- package/templates/skills/triage-automation/SKILL.md +134 -0
- package/templates/skills/triage-inbox/SKILL.md +225 -0
- package/templates/skills/writing-specs/SKILL.md +347 -0
|
@@ -17,185 +17,12 @@ import { registerAdapter, resolveAdapter, } from "../../agents/index.js";
|
|
|
17
17
|
import { spawnAndInitialize } from "../../agents/spawner.js";
|
|
18
18
|
import { initContext, loadAllItems, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
|
|
19
19
|
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,
|
|
20
|
+
import { appendEvent, closeSession, createSessionWithBudget, getSessionBudgetPath, isEndLoopRequested, requestEndLoop, resetBudget, saveSessionContext, } from "../../sessions/index.js";
|
|
21
21
|
import { errors } from "../../strings/index.js";
|
|
22
22
|
import { getCurrentBranch } from "../../utils/git.js";
|
|
23
23
|
import { EXIT_CODES } from "../exit-codes.js";
|
|
24
24
|
import { error, info, success, warn } from "../output.js";
|
|
25
|
-
import { gatherSessionContext,
|
|
26
|
-
const TASK_LIMIT_MARKER_PATH = ".claude/ralph-task-limit.json";
|
|
27
|
-
const END_LOOP_MARKER_PATH = ".claude/ralph-end-loop.json";
|
|
28
|
-
const STALE_MARKER_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
|
|
29
|
-
/**
|
|
30
|
-
* Write task limit marker file.
|
|
31
|
-
* AC: @ralph-task-limit ac-wrapup, ac-marker-format
|
|
32
|
-
*/
|
|
33
|
-
async function writeTaskLimitMarker(rootDir, marker) {
|
|
34
|
-
const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
|
|
35
|
-
const dir = path.dirname(markerPath);
|
|
36
|
-
await fs.mkdir(dir, { recursive: true });
|
|
37
|
-
await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Read task limit marker file if it exists.
|
|
41
|
-
*/
|
|
42
|
-
async function readTaskLimitMarker(rootDir) {
|
|
43
|
-
const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
|
|
44
|
-
try {
|
|
45
|
-
const content = await fs.readFile(markerPath, "utf-8");
|
|
46
|
-
return JSON.parse(content);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Clear task limit marker file.
|
|
54
|
-
* AC: @ralph-task-limit ac-reset
|
|
55
|
-
*/
|
|
56
|
-
async function clearTaskLimitMarker(rootDir) {
|
|
57
|
-
const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
|
|
58
|
-
try {
|
|
59
|
-
await fs.unlink(markerPath);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
// Ignore if file doesn't exist
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Clear stale marker files (older than 1 hour).
|
|
67
|
-
* AC: @ralph-task-limit ac-reset
|
|
68
|
-
*/
|
|
69
|
-
async function clearStaleMarker(rootDir) {
|
|
70
|
-
const marker = await readTaskLimitMarker(rootDir);
|
|
71
|
-
if (!marker)
|
|
72
|
-
return false;
|
|
73
|
-
const markerAge = Date.now() - new Date(marker.since).getTime();
|
|
74
|
-
if (markerAge > STALE_MARKER_THRESHOLD_MS) {
|
|
75
|
-
await clearTaskLimitMarker(rootDir);
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Write end-loop marker file.
|
|
82
|
-
* AC: @ralph-end-loop ac-cmd
|
|
83
|
-
*/
|
|
84
|
-
async function writeEndLoopMarker(rootDir, reason) {
|
|
85
|
-
const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
|
|
86
|
-
const dir = path.dirname(markerPath);
|
|
87
|
-
await fs.mkdir(dir, { recursive: true });
|
|
88
|
-
const marker = {
|
|
89
|
-
requested: true,
|
|
90
|
-
timestamp: new Date().toISOString(),
|
|
91
|
-
reason,
|
|
92
|
-
};
|
|
93
|
-
await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Read end-loop marker file if it exists.
|
|
97
|
-
* AC: @ralph-end-loop ac-detect
|
|
98
|
-
*/
|
|
99
|
-
async function readEndLoopMarker(rootDir) {
|
|
100
|
-
const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
|
|
101
|
-
try {
|
|
102
|
-
const content = await fs.readFile(markerPath, "utf-8");
|
|
103
|
-
return JSON.parse(content);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Clear end-loop marker file.
|
|
111
|
-
* AC: @ralph-end-loop ac-cleanup
|
|
112
|
-
*/
|
|
113
|
-
async function clearEndLoopMarker(rootDir) {
|
|
114
|
-
const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
|
|
115
|
-
try {
|
|
116
|
-
await fs.unlink(markerPath);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Ignore if file doesn't exist
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Clear stale end-loop markers (older than 1 hour).
|
|
124
|
-
* AC: @ralph-end-loop ac-cleanup
|
|
125
|
-
*/
|
|
126
|
-
async function clearStaleEndLoopMarker(rootDir) {
|
|
127
|
-
const marker = await readEndLoopMarker(rootDir);
|
|
128
|
-
if (!marker)
|
|
129
|
-
return false;
|
|
130
|
-
const markerAge = Date.now() - new Date(marker.timestamp).getTime();
|
|
131
|
-
if (markerAge > STALE_MARKER_THRESHOLD_MS) {
|
|
132
|
-
await clearEndLoopMarker(rootDir);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Detect if a Bash command is a task complete command.
|
|
139
|
-
* AC: @ralph-task-limit ac-detection
|
|
140
|
-
*/
|
|
141
|
-
function detectTaskCompleteCommand(command) {
|
|
142
|
-
// Match variations of "kspec task complete"
|
|
143
|
-
// Don't match "kspec task submit" - that's just status change to pending_review
|
|
144
|
-
return /\bkspec\s+task\s+complete\b/.test(command);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Detect if a Bash command is an end-loop command.
|
|
148
|
-
* AC: @ralph-end-loop ac-detect
|
|
149
|
-
*/
|
|
150
|
-
function detectEndLoopCommand(command) {
|
|
151
|
-
// Match "kspec ralph end-loop" with any arguments
|
|
152
|
-
return /\bkspec\s+ralph\s+end-loop\b/.test(command);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Extract Bash command from SessionUpdate if it's a tool_call or tool_call_update event.
|
|
156
|
-
* Returns null if not a Bash tool call.
|
|
157
|
-
*/
|
|
158
|
-
function extractBashCommand(update) {
|
|
159
|
-
const u = update;
|
|
160
|
-
// Check if this is a tool call event
|
|
161
|
-
if (u.sessionUpdate !== "tool_call" && u.sessionUpdate !== "tool_call_update") {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
// Extract tool name - check various locations Claude Code uses
|
|
165
|
-
let toolName;
|
|
166
|
-
// Try _meta.claudeCode.toolName first (Claude Code pattern)
|
|
167
|
-
const meta = u._meta;
|
|
168
|
-
if (meta) {
|
|
169
|
-
const claudeCode = meta.claudeCode;
|
|
170
|
-
if (claudeCode?.toolName) {
|
|
171
|
-
toolName = String(claudeCode.toolName);
|
|
172
|
-
}
|
|
173
|
-
else if (meta.toolName) {
|
|
174
|
-
toolName = String(meta.toolName);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Fall back to name or title field
|
|
178
|
-
if (!toolName && u.name) {
|
|
179
|
-
toolName = String(u.name);
|
|
180
|
-
}
|
|
181
|
-
if (!toolName && u.title) {
|
|
182
|
-
toolName = String(u.title);
|
|
183
|
-
}
|
|
184
|
-
// Check if it's a Bash tool (handle MCP prefix variations)
|
|
185
|
-
if (!toolName)
|
|
186
|
-
return null;
|
|
187
|
-
const isBash = toolName === "Bash" || toolName.endsWith("__Bash");
|
|
188
|
-
if (!isBash)
|
|
189
|
-
return null;
|
|
190
|
-
// Extract command from input
|
|
191
|
-
const input = (u.rawInput || u.input || u.params);
|
|
192
|
-
if (!input)
|
|
193
|
-
return null;
|
|
194
|
-
const command = input.command;
|
|
195
|
-
if (typeof command !== "string")
|
|
196
|
-
return null;
|
|
197
|
-
return command;
|
|
198
|
-
}
|
|
25
|
+
import { gatherSessionContext, } from "./session.js";
|
|
199
26
|
/**
|
|
200
27
|
* Parse and validate --tasks flag value.
|
|
201
28
|
* Returns resolved ULIDs for the specified task refs.
|
|
@@ -266,7 +93,7 @@ async function allExplicitTasksDone(ctx, scope) {
|
|
|
266
93
|
}
|
|
267
94
|
// ─── Prompt Template ─────────────────────────────────────────────────────────
|
|
268
95
|
// AC: @ralph-skill-delegation ac-1, ac-2, ac-3
|
|
269
|
-
function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, focus, explicitTaskScope) {
|
|
96
|
+
function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, skillTaskWork, focus, explicitTaskScope) {
|
|
270
97
|
const focusSection = focus
|
|
271
98
|
? `
|
|
272
99
|
## Session Focus (applies to ALL iterations)
|
|
@@ -306,7 +133,7 @@ ${JSON.stringify(sessionCtx, null, 2)}
|
|
|
306
133
|
Run the task-work skill in loop mode:
|
|
307
134
|
|
|
308
135
|
\`\`\`
|
|
309
|
-
|
|
136
|
+
${skillTaskWork} loop
|
|
310
137
|
\`\`\`
|
|
311
138
|
|
|
312
139
|
${modeDescription}
|
|
@@ -322,7 +149,7 @@ it checks for remaining eligible tasks at the start of each iteration and exits
|
|
|
322
149
|
* Build the reflect prompt sent after task-work completes.
|
|
323
150
|
* Ralph sends this as a separate prompt to ensure reflection always happens.
|
|
324
151
|
*/
|
|
325
|
-
function buildReflectPrompt(iteration, maxLoops, sessionId) {
|
|
152
|
+
function buildReflectPrompt(iteration, maxLoops, sessionId, skillReflect) {
|
|
326
153
|
const isFinal = iteration === maxLoops;
|
|
327
154
|
return `# Kspec Automation Session - Reflection
|
|
328
155
|
|
|
@@ -335,7 +162,7 @@ function buildReflectPrompt(iteration, maxLoops, sessionId) {
|
|
|
335
162
|
Run the reflect skill in loop mode:
|
|
336
163
|
|
|
337
164
|
\`\`\`
|
|
338
|
-
|
|
165
|
+
${skillReflect} loop
|
|
339
166
|
\`\`\`
|
|
340
167
|
|
|
341
168
|
Loop mode means: high-confidence captures only, must search existing before capturing, no user prompts.
|
|
@@ -661,6 +488,7 @@ async function processPendingReviewTasks(ctx, adapter, pendingReviewTasks, optio
|
|
|
661
488
|
const result = await runSubagent(adapter, subagentCtx, {
|
|
662
489
|
timeout: options.subagentTimeout,
|
|
663
490
|
outputPrefix: DEFAULT_SUBAGENT_PREFIX,
|
|
491
|
+
skillName: ctx.config.ralph.skills.pr_review,
|
|
664
492
|
}, {
|
|
665
493
|
yolo: options.yolo,
|
|
666
494
|
cwd: options.cwd,
|
|
@@ -734,7 +562,7 @@ export function registerRalphCommand(program) {
|
|
|
734
562
|
.command("ralph")
|
|
735
563
|
.description("Ralph automated task loop and agent control");
|
|
736
564
|
// end-loop subcommand - allows agent to signal loop termination
|
|
737
|
-
// AC: @
|
|
565
|
+
// AC: @session-end-loop-signal ac-signal
|
|
738
566
|
ralph
|
|
739
567
|
.command("end-loop")
|
|
740
568
|
.description("End the ralph loop gracefully (stops all remaining iterations)")
|
|
@@ -742,26 +570,31 @@ export function registerRalphCommand(program) {
|
|
|
742
570
|
.action(async (options) => {
|
|
743
571
|
try {
|
|
744
572
|
const ctx = await initContext();
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
// AC: @ralph-end-loop ac-noop-outside
|
|
754
|
-
warn("No active ralph session detected. Marker written but may have no effect.");
|
|
755
|
-
info("This command is designed to be called by agents during a ralph loop.");
|
|
573
|
+
const sessionId = process.env.KSPEC_SESSION_ID;
|
|
574
|
+
if (!sessionId) {
|
|
575
|
+
// AC: @trait-error-guidance ac-1, ac-2
|
|
576
|
+
warn("No active ralph session detected (KSPEC_SESSION_ID not set).");
|
|
577
|
+
info("This command requires an active session. It is designed to be called by agents during a ralph loop.");
|
|
578
|
+
info("Suggestion: Ensure KSPEC_SESSION_ID is set, or start a session with: kspec session create --agent-type ralph");
|
|
579
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
580
|
+
return;
|
|
756
581
|
}
|
|
757
|
-
|
|
758
|
-
|
|
582
|
+
// AC: @session-end-loop-signal ac-signal - Write end-loop state to session
|
|
583
|
+
const updated = await requestEndLoop(ctx.specDir, sessionId, options.reason);
|
|
584
|
+
if (!updated) {
|
|
585
|
+
// AC: @trait-error-guidance ac-1, ac-2
|
|
586
|
+
error(`Session not found: ${sessionId}`);
|
|
587
|
+
info("Suggestion: Check session ID with: kspec session log list");
|
|
588
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
589
|
+
return;
|
|
759
590
|
}
|
|
591
|
+
success("Loop end signal sent");
|
|
760
592
|
if (options.reason) {
|
|
761
593
|
info(`Reason: ${options.reason}`);
|
|
762
594
|
}
|
|
763
595
|
}
|
|
764
596
|
catch (err) {
|
|
597
|
+
// AC: @trait-error-guidance ac-1
|
|
765
598
|
error("Failed to signal end-loop", err);
|
|
766
599
|
process.exit(EXIT_CODES.ERROR);
|
|
767
600
|
}
|
|
@@ -829,7 +662,7 @@ export function registerRalphCommand(program) {
|
|
|
829
662
|
error("--restart-every must be a non-negative integer");
|
|
830
663
|
process.exit(EXIT_CODES.ERROR);
|
|
831
664
|
}
|
|
832
|
-
// AC: @ralph-
|
|
665
|
+
// AC: @ralph-session-budget-integration ac-create-budget
|
|
833
666
|
const maxTasks = parseInt(options.maxTasks, 10);
|
|
834
667
|
if (Number.isNaN(maxTasks) || maxTasks < 0 || maxTasks > 999) {
|
|
835
668
|
error("--max-tasks must be 0 (unlimited) or a positive integer up to 999");
|
|
@@ -894,85 +727,96 @@ export function registerRalphCommand(program) {
|
|
|
894
727
|
const specDir = ctx.specDir;
|
|
895
728
|
// Create session for event tracking
|
|
896
729
|
const sessionId = ulid();
|
|
897
|
-
|
|
730
|
+
// Set session env vars on this process so all spawned agents
|
|
731
|
+
// (main worker, subagent, wrap-up) inherit them via process.env.
|
|
732
|
+
// KSPEC_RALPH_SESSION: Used by codex skill safety guard to detect ralph context.
|
|
733
|
+
// KSPEC_SESSION_ID: Used by kspec task start for budget enforcement.
|
|
734
|
+
// AC: @ralph-session-budget-integration ac-env-inject
|
|
735
|
+
process.env.KSPEC_RALPH_SESSION = sessionId;
|
|
736
|
+
process.env.KSPEC_SESSION_ID = sessionId;
|
|
737
|
+
// AC: @ralph-session-budget-integration ac-create-budget
|
|
738
|
+
// Create session with budget. When maxTasks=0 (unlimited), no budget.json is created.
|
|
739
|
+
await createSessionWithBudget(specDir, {
|
|
898
740
|
id: sessionId,
|
|
899
741
|
agent_type: options.adapter,
|
|
900
|
-
|
|
901
|
-
});
|
|
902
|
-
// Log session start
|
|
903
|
-
await appendEvent(specDir, {
|
|
904
|
-
session_id: sessionId,
|
|
905
|
-
type: "session.start",
|
|
906
|
-
data: {
|
|
907
|
-
adapter: options.adapter,
|
|
908
|
-
maxLoops,
|
|
909
|
-
maxRetries,
|
|
910
|
-
maxFailures,
|
|
911
|
-
maxTasks,
|
|
912
|
-
yolo: options.yolo,
|
|
913
|
-
focus: options.focus,
|
|
914
|
-
explicitTasks: explicitTaskScope?.refs,
|
|
915
|
-
},
|
|
742
|
+
budget: maxTasks,
|
|
916
743
|
});
|
|
744
|
+
// Everything after session creation is wrapped in try/finally to guarantee
|
|
745
|
+
// budget cleanup even if pre-loop setup (event logging, signal handlers) throws.
|
|
746
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
917
747
|
let consecutiveFailures = 0;
|
|
918
748
|
let agent = null;
|
|
919
749
|
let acpSessionId = null;
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
750
|
+
let exitReason = null;
|
|
751
|
+
let lastIterationCtx = null;
|
|
752
|
+
let lastErrorMessage;
|
|
753
|
+
const recentTaskRefs = [];
|
|
754
|
+
const sessionIterationMap = new Map();
|
|
755
|
+
// Signal handler refs — declared here so finally can remove them
|
|
756
|
+
// AC: @ralph-task-limit ac-signal-cleanup
|
|
924
757
|
const signalCleanup = (signal) => {
|
|
925
758
|
info(`Received ${signal}, cleaning up...`);
|
|
926
|
-
// Kill agent if running
|
|
927
759
|
if (agent) {
|
|
928
760
|
agent.kill();
|
|
929
761
|
}
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
762
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
763
|
+
// Must use async IIFE — signal handlers are called synchronously,
|
|
764
|
+
// but cleanup needs async I/O. The IIFE keeps the event loop alive
|
|
765
|
+
// until cleanup completes, then exits explicitly.
|
|
766
|
+
void (async () => {
|
|
767
|
+
try {
|
|
768
|
+
await Promise.all([
|
|
769
|
+
fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { }),
|
|
770
|
+
closeSession(specDir, sessionId, "abandoned", `Received ${signal}`),
|
|
771
|
+
]);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Best-effort cleanup — don't let errors prevent exit
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
process.exit(0);
|
|
778
|
+
}
|
|
779
|
+
})();
|
|
937
780
|
};
|
|
938
781
|
const sigintHandler = () => { signalCleanup("SIGINT"); };
|
|
939
782
|
const sigtermHandler = () => { signalCleanup("SIGTERM"); };
|
|
940
|
-
process.on("SIGINT", sigintHandler);
|
|
941
|
-
process.on("SIGTERM", sigtermHandler);
|
|
942
|
-
// Create translator and renderer for this session
|
|
943
|
-
const translator = createTranslator();
|
|
944
|
-
const renderer = createCliRenderer();
|
|
945
|
-
// Task limit state - tracks completions per iteration
|
|
946
|
-
// AC: @ralph-task-limit ac-reset, ac-wrapup
|
|
947
|
-
let taskLimitReached = false;
|
|
948
|
-
let tasksCompletedThisIteration = 0;
|
|
949
|
-
// End-loop signal state
|
|
950
|
-
// AC: @ralph-end-loop ac-detect, ac-graceful
|
|
951
|
-
let endLoopRequested = false;
|
|
952
|
-
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1 - Track exit reason for wrap-up
|
|
953
|
-
let exitReason = null;
|
|
954
|
-
let lastIterationCtx = null;
|
|
955
|
-
let lastErrorMessage;
|
|
956
|
-
const recentTaskRefs = [];
|
|
957
783
|
try {
|
|
784
|
+
// AC: @session-end-loop-signal ac-session-close-signal
|
|
785
|
+
// Install signal handlers FIRST, before any async work, so signals
|
|
786
|
+
// during startup (e.g. during appendEvent) still trigger cleanup.
|
|
787
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
788
|
+
process.on("SIGINT", sigintHandler);
|
|
789
|
+
process.on("SIGTERM", sigtermHandler);
|
|
790
|
+
// Log session start
|
|
791
|
+
await appendEvent(specDir, {
|
|
792
|
+
session_id: sessionId,
|
|
793
|
+
type: "session.start",
|
|
794
|
+
data: {
|
|
795
|
+
adapter: options.adapter,
|
|
796
|
+
maxLoops,
|
|
797
|
+
maxRetries,
|
|
798
|
+
maxFailures,
|
|
799
|
+
maxTasks,
|
|
800
|
+
yolo: options.yolo,
|
|
801
|
+
focus: options.focus,
|
|
802
|
+
explicitTasks: explicitTaskScope?.refs,
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
// Create translator and renderer for this session
|
|
806
|
+
const translator = createTranslator();
|
|
807
|
+
const renderer = createCliRenderer();
|
|
958
808
|
for (let iteration = 1; iteration <= maxLoops; iteration++) {
|
|
959
809
|
renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
|
|
960
|
-
// AC: @ralph-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
// AC: @ralph-end-loop ac-cleanup - Reset end-loop state
|
|
970
|
-
endLoopRequested = false;
|
|
971
|
-
const wasStaleEndLoop = await clearStaleEndLoopMarker(ctx.rootDir);
|
|
972
|
-
if (wasStaleEndLoop) {
|
|
973
|
-
info("Cleared stale end-loop marker from previous session");
|
|
810
|
+
// AC: @ralph-session-budget-integration ac-reset-iteration
|
|
811
|
+
// Reset budget counter at iteration start (no-op when no budget exists)
|
|
812
|
+
await resetBudget(specDir, sessionId);
|
|
813
|
+
// AC: @session-end-loop-signal ac-detect - Check session state for end-loop
|
|
814
|
+
const endLoopState = await isEndLoopRequested(specDir, sessionId);
|
|
815
|
+
if (endLoopState?.requested) {
|
|
816
|
+
info(`End-loop already requested for this session. Exiting.`);
|
|
817
|
+
exitReason = "end_loop_signal";
|
|
818
|
+
break;
|
|
974
819
|
}
|
|
975
|
-
await clearEndLoopMarker(ctx.rootDir);
|
|
976
820
|
// Gather fresh context each iteration
|
|
977
821
|
// AC: @cli-ralph ac-16 - Only automation-eligible tasks (unless explicit scope)
|
|
978
822
|
// AC: @cli-ralph ac-21 - With explicit task scope, ignore automation eligibility
|
|
@@ -1046,9 +890,9 @@ export function registerRalphCommand(program) {
|
|
|
1046
890
|
const iterationStartTime = new Date();
|
|
1047
891
|
// Build prompts - task-work first, then reflect
|
|
1048
892
|
// AC: @cli-ralph ac-21 - Include explicit task scope in prompt
|
|
1049
|
-
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, options.focus, explicitTaskScope);
|
|
1050
|
-
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId);
|
|
1051
|
-
// AC: @
|
|
893
|
+
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, ctx.config.ralph.skills.task_work, options.focus, explicitTaskScope);
|
|
894
|
+
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId, ctx.config.ralph.skills.reflect);
|
|
895
|
+
// AC: @cli-ralph ac-21
|
|
1052
896
|
if (options.dryRun) {
|
|
1053
897
|
console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
|
|
1054
898
|
console.log(` max-loops: ${maxLoops}`);
|
|
@@ -1091,8 +935,10 @@ export function registerRalphCommand(program) {
|
|
|
1091
935
|
// Spawn agent if not already running
|
|
1092
936
|
if (!agent) {
|
|
1093
937
|
info("Spawning ACP agent...");
|
|
938
|
+
// AC: @ralph-session-budget-integration ac-env-inject
|
|
1094
939
|
agent = await spawnAndInitialize(adapter, {
|
|
1095
940
|
cwd: process.cwd(),
|
|
941
|
+
env: { KSPEC_SESSION_ID: sessionId },
|
|
1096
942
|
clientOptions: {
|
|
1097
943
|
clientInfo: {
|
|
1098
944
|
name: "kspec-ralph",
|
|
@@ -1111,69 +957,14 @@ export function registerRalphCommand(program) {
|
|
|
1111
957
|
if (event) {
|
|
1112
958
|
renderer.render(event);
|
|
1113
959
|
}
|
|
1114
|
-
// AC: @ralph-task-limit ac-detection, ac-wrapup
|
|
1115
|
-
// Detect task completions for limit enforcement
|
|
1116
|
-
if (maxTasks > 0 && !taskLimitReached) {
|
|
1117
|
-
const bashCmd = extractBashCommand(update);
|
|
1118
|
-
if (bashCmd && detectTaskCompleteCommand(bashCmd)) {
|
|
1119
|
-
// Pattern matched - verify via kspec query
|
|
1120
|
-
getIterationStats(ctx, iterationStartTime)
|
|
1121
|
-
.then(async (stats) => {
|
|
1122
|
-
if (stats.tasks_completed >= maxTasks && !taskLimitReached) {
|
|
1123
|
-
taskLimitReached = true;
|
|
1124
|
-
tasksCompletedThisIteration = stats.tasks_completed;
|
|
1125
|
-
info(`Task limit reached (${stats.tasks_completed}/${maxTasks})`);
|
|
1126
|
-
// AC: @ralph-task-limit ac-marker-format, ac-wrapup
|
|
1127
|
-
// Write marker file for hook enforcement
|
|
1128
|
-
const marker = {
|
|
1129
|
-
active: true,
|
|
1130
|
-
since: iterationStartTime.toISOString(),
|
|
1131
|
-
max: maxTasks,
|
|
1132
|
-
completed: stats.tasks_completed,
|
|
1133
|
-
sessionId,
|
|
1134
|
-
};
|
|
1135
|
-
await writeTaskLimitMarker(ctx.rootDir, marker);
|
|
1136
|
-
// Inject wrap-up message to agent
|
|
1137
|
-
if (agent && acpSessionId) {
|
|
1138
|
-
const wrapUpMsg = `\n\n**TASK LIMIT REACHED** - ${stats.tasks_completed} task(s) completed this iteration (limit: ${maxTasks}).\n\nPlease wrap up your current work and exit cleanly. Do not start new tasks.\n\nCompleted tasks this iteration: ${stats.completed_refs.join(", ")}`;
|
|
1139
|
-
agent.client.prompt({
|
|
1140
|
-
sessionId: acpSessionId,
|
|
1141
|
-
prompt: [{ type: "text", text: wrapUpMsg }],
|
|
1142
|
-
}).catch(() => {
|
|
1143
|
-
// Ignore if message injection fails
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
})
|
|
1148
|
-
.catch(() => {
|
|
1149
|
-
// Ignore query failures - detection is best-effort
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
// AC: @ralph-end-loop ac-detect
|
|
1154
|
-
// Detect explicit end-loop command
|
|
1155
|
-
if (!endLoopRequested) {
|
|
1156
|
-
const bashCmd = extractBashCommand(update);
|
|
1157
|
-
if (bashCmd && detectEndLoopCommand(bashCmd)) {
|
|
1158
|
-
endLoopRequested = true;
|
|
1159
|
-
// Read marker to get reason if present
|
|
1160
|
-
readEndLoopMarker(ctx.rootDir)
|
|
1161
|
-
.then((marker) => {
|
|
1162
|
-
const reason = marker?.reason
|
|
1163
|
-
? ` (${marker.reason})`
|
|
1164
|
-
: "";
|
|
1165
|
-
info(`End-loop signal received${reason}`);
|
|
1166
|
-
})
|
|
1167
|
-
.catch(() => {
|
|
1168
|
-
info("End-loop signal received");
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
960
|
// Log raw update event (async, non-blocking)
|
|
961
|
+
// Look up iteration by ACP session ID so late updates from
|
|
962
|
+
// a previous session are attributed to the correct iteration
|
|
963
|
+
const eventIteration = sessionIterationMap.get(_sid) ?? 0;
|
|
1173
964
|
appendEvent(specDir, {
|
|
1174
965
|
session_id: sessionId,
|
|
1175
966
|
type: "session.update",
|
|
1176
|
-
data: { iteration, update },
|
|
967
|
+
data: { iteration: eventIteration, update },
|
|
1177
968
|
}).catch(() => {
|
|
1178
969
|
// Ignore logging errors during streaming
|
|
1179
970
|
});
|
|
@@ -1193,6 +984,7 @@ export function registerRalphCommand(program) {
|
|
|
1193
984
|
cwd: process.cwd(),
|
|
1194
985
|
mcpServers: [], // No MCP servers for now
|
|
1195
986
|
});
|
|
987
|
+
sessionIterationMap.set(acpSessionId, iteration);
|
|
1196
988
|
// Phase 1: Task Work
|
|
1197
989
|
info("Sending task-work prompt to agent...");
|
|
1198
990
|
const taskWorkResponse = await agent.client.prompt({
|
|
@@ -1269,12 +1061,6 @@ export function registerRalphCommand(program) {
|
|
|
1269
1061
|
}
|
|
1270
1062
|
}
|
|
1271
1063
|
lastIterationCtx = sessionCtx;
|
|
1272
|
-
// AC: @ralph-end-loop ac-graceful - Check for end-loop signal
|
|
1273
|
-
if (endLoopRequested) {
|
|
1274
|
-
info("Agent requested end of loop. Exiting gracefully.");
|
|
1275
|
-
exitReason = "end_loop_signal";
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1278
1064
|
// Periodic agent restart to prevent OOM
|
|
1279
1065
|
// AC: @cli-ralph ac-restart-periodic
|
|
1280
1066
|
if (restartEvery > 0 &&
|
|
@@ -1312,6 +1098,13 @@ export function registerRalphCommand(program) {
|
|
|
1312
1098
|
exitReason = "max_iterations";
|
|
1313
1099
|
}
|
|
1314
1100
|
}
|
|
1101
|
+
catch (loopErr) {
|
|
1102
|
+
// AC: @session-end-loop-signal ac-session-close-error
|
|
1103
|
+
// Unrecoverable error during loop execution
|
|
1104
|
+
exitReason = exitReason ?? "error";
|
|
1105
|
+
lastErrorMessage = loopErr.message;
|
|
1106
|
+
error("Unrecoverable error in ralph loop", loopErr);
|
|
1107
|
+
}
|
|
1315
1108
|
finally {
|
|
1316
1109
|
// Remove signal handlers to avoid double cleanup
|
|
1317
1110
|
process.off("SIGINT", sigintHandler);
|
|
@@ -1321,10 +1114,12 @@ export function registerRalphCommand(program) {
|
|
|
1321
1114
|
agent.kill();
|
|
1322
1115
|
agent = null;
|
|
1323
1116
|
}
|
|
1324
|
-
// AC: @ralph-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1117
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1118
|
+
// Clean up budget file when session ends
|
|
1119
|
+
await fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { });
|
|
1120
|
+
// Clean up session env vars
|
|
1121
|
+
delete process.env.KSPEC_RALPH_SESSION;
|
|
1122
|
+
delete process.env.KSPEC_SESSION_ID;
|
|
1328
1123
|
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1, ac-2, ac-3, ac-4, ac-5
|
|
1329
1124
|
// Spawn wrap-up agent if not dry-run and we have an exit reason
|
|
1330
1125
|
if (!options.dryRun && exitReason) {
|
|
@@ -1371,8 +1166,25 @@ export function registerRalphCommand(program) {
|
|
|
1371
1166
|
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1372
1167
|
console.log("");
|
|
1373
1168
|
}
|
|
1374
|
-
// Log session end
|
|
1375
|
-
|
|
1169
|
+
// Log session end and close session with appropriate status/reason
|
|
1170
|
+
// AC: @session-end-loop-signal ac-session-close-normal, ac-session-close-error
|
|
1171
|
+
const isErrorExit = consecutiveFailures >= maxFailures ||
|
|
1172
|
+
exitReason === "max_failures" ||
|
|
1173
|
+
exitReason === "error";
|
|
1174
|
+
const status = isErrorExit ? "abandoned" : "completed";
|
|
1175
|
+
const closeReason = exitReason === "max_failures"
|
|
1176
|
+
? `Max failures reached (${consecutiveFailures}/${maxFailures})${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
|
|
1177
|
+
: exitReason === "error"
|
|
1178
|
+
? `Unrecoverable error${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
|
|
1179
|
+
: exitReason === "end_loop_signal"
|
|
1180
|
+
? "Agent requested end of loop"
|
|
1181
|
+
: exitReason === "max_iterations"
|
|
1182
|
+
? `Completed all ${maxLoops} iterations`
|
|
1183
|
+
: exitReason === "no_tasks"
|
|
1184
|
+
? "No eligible tasks remaining"
|
|
1185
|
+
: exitReason === "explicit_tasks_done"
|
|
1186
|
+
? "All explicit tasks completed"
|
|
1187
|
+
: `Loop ended: ${exitReason}`;
|
|
1376
1188
|
await appendEvent(specDir, {
|
|
1377
1189
|
session_id: sessionId,
|
|
1378
1190
|
type: "session.end",
|
|
@@ -1380,9 +1192,10 @@ export function registerRalphCommand(program) {
|
|
|
1380
1192
|
status,
|
|
1381
1193
|
consecutiveFailures,
|
|
1382
1194
|
exitReason,
|
|
1195
|
+
closeReason,
|
|
1383
1196
|
},
|
|
1384
1197
|
});
|
|
1385
|
-
await
|
|
1198
|
+
await closeSession(specDir, sessionId, status, closeReason);
|
|
1386
1199
|
}
|
|
1387
1200
|
console.log(chalk.green(`\n${"─".repeat(60)}`));
|
|
1388
1201
|
success("Ralph loop completed");
|