@kynetic-ai/spec 0.4.0 → 0.6.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/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 +60 -23
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/plan-import.js +51 -12
- 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 +144 -329
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts +19 -0
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -0
- package/dist/cli/commands/session/checkpoint.js +161 -0
- package/dist/cli/commands/session/checkpoint.js.map +1 -0
- package/dist/cli/commands/session/commands.d.ts +18 -0
- package/dist/cli/commands/session/commands.d.ts.map +1 -0
- package/dist/cli/commands/session/commands.js +259 -0
- package/dist/cli/commands/session/commands.js.map +1 -0
- package/dist/cli/commands/session/context.d.ts +17 -0
- package/dist/cli/commands/session/context.d.ts.map +1 -0
- package/dist/cli/commands/session/context.js +493 -0
- package/dist/cli/commands/session/context.js.map +1 -0
- package/dist/cli/commands/session/create.d.ts +29 -0
- package/dist/cli/commands/session/create.d.ts.map +1 -0
- package/dist/cli/commands/session/create.js +147 -0
- package/dist/cli/commands/session/create.js.map +1 -0
- package/dist/cli/commands/session/format.d.ts +27 -0
- package/dist/cli/commands/session/format.d.ts.map +1 -0
- package/dist/cli/commands/session/format.js +401 -0
- package/dist/cli/commands/session/format.js.map +1 -0
- package/dist/cli/commands/session/index.d.ts +13 -0
- package/dist/cli/commands/session/index.d.ts.map +1 -0
- package/dist/cli/commands/session/index.js +17 -0
- package/dist/cli/commands/session/index.js.map +1 -0
- package/dist/cli/commands/session/log.d.ts +52 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -0
- package/dist/cli/commands/session/log.js +570 -0
- package/dist/cli/commands/session/log.js.map +1 -0
- package/dist/cli/commands/session/types.d.ts +230 -0
- package/dist/cli/commands/session/types.d.ts.map +1 -0
- package/dist/cli/commands/session/types.js +7 -0
- package/dist/cli/commands/session/types.js.map +1 -0
- package/dist/cli/commands/session.d.ts +4 -179
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +6 -1424
- 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 +69 -223
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +95 -37
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +23 -7
- package/dist/cli/commands/validate.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 +14 -2
- package/dist/cli/output.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 +36 -0
- package/dist/parser/plan-document.d.ts.map +1 -1
- package/dist/parser/plan-document.js +75 -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 +5 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +29 -17
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/validate.d.ts +4 -1
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +50 -35
- package/dist/parser/validate.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/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 +254 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +621 -1
- 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/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/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 +4 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/review/SKILL.md +37 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +16 -0
- package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +1 -1
- package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +14 -0
- package/templates/agents-sections/05-commit-convention.md +14 -0
- package/templates/skills/review/SKILL.md +37 -0
- package/templates/skills/task-work/SKILL.md +16 -0
- package/templates/skills/triage-inbox/SKILL.md +1 -1
- package/templates/skills/writing-specs/SKILL.md +14 -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, injectEnvForAdapter, isEndLoopRequested, removeEnvForAdapter, 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.
|
|
@@ -735,7 +562,7 @@ export function registerRalphCommand(program) {
|
|
|
735
562
|
.command("ralph")
|
|
736
563
|
.description("Ralph automated task loop and agent control");
|
|
737
564
|
// end-loop subcommand - allows agent to signal loop termination
|
|
738
|
-
// AC: @
|
|
565
|
+
// AC: @session-end-loop-signal ac-signal
|
|
739
566
|
ralph
|
|
740
567
|
.command("end-loop")
|
|
741
568
|
.description("End the ralph loop gracefully (stops all remaining iterations)")
|
|
@@ -743,26 +570,31 @@ export function registerRalphCommand(program) {
|
|
|
743
570
|
.action(async (options) => {
|
|
744
571
|
try {
|
|
745
572
|
const ctx = await initContext();
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// AC: @ralph-end-loop ac-noop-outside
|
|
755
|
-
warn("No active ralph session detected. Marker written but may have no effect.");
|
|
756
|
-
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;
|
|
757
581
|
}
|
|
758
|
-
|
|
759
|
-
|
|
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;
|
|
760
590
|
}
|
|
591
|
+
success("Loop end signal sent");
|
|
761
592
|
if (options.reason) {
|
|
762
593
|
info(`Reason: ${options.reason}`);
|
|
763
594
|
}
|
|
764
595
|
}
|
|
765
596
|
catch (err) {
|
|
597
|
+
// AC: @trait-error-guidance ac-1
|
|
766
598
|
error("Failed to signal end-loop", err);
|
|
767
599
|
process.exit(EXIT_CODES.ERROR);
|
|
768
600
|
}
|
|
@@ -830,7 +662,7 @@ export function registerRalphCommand(program) {
|
|
|
830
662
|
error("--restart-every must be a non-negative integer");
|
|
831
663
|
process.exit(EXIT_CODES.ERROR);
|
|
832
664
|
}
|
|
833
|
-
// AC: @ralph-
|
|
665
|
+
// AC: @ralph-session-budget-integration ac-create-budget
|
|
834
666
|
const maxTasks = parseInt(options.maxTasks, 10);
|
|
835
667
|
if (Number.isNaN(maxTasks) || maxTasks < 0 || maxTasks > 999) {
|
|
836
668
|
error("--max-tasks must be 0 (unlimited) or a positive integer up to 999");
|
|
@@ -895,91 +727,108 @@ export function registerRalphCommand(program) {
|
|
|
895
727
|
const specDir = ctx.specDir;
|
|
896
728
|
// Create session for event tracking
|
|
897
729
|
const sessionId = ulid();
|
|
898
|
-
|
|
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, {
|
|
899
740
|
id: sessionId,
|
|
900
741
|
agent_type: options.adapter,
|
|
901
|
-
|
|
902
|
-
});
|
|
903
|
-
// Log session start
|
|
904
|
-
await appendEvent(specDir, {
|
|
905
|
-
session_id: sessionId,
|
|
906
|
-
type: "session.start",
|
|
907
|
-
data: {
|
|
908
|
-
adapter: options.adapter,
|
|
909
|
-
maxLoops,
|
|
910
|
-
maxRetries,
|
|
911
|
-
maxFailures,
|
|
912
|
-
maxTasks,
|
|
913
|
-
yolo: options.yolo,
|
|
914
|
-
focus: options.focus,
|
|
915
|
-
explicitTasks: explicitTaskScope?.refs,
|
|
916
|
-
},
|
|
742
|
+
budget: maxTasks,
|
|
917
743
|
});
|
|
744
|
+
// Adapter ID for harness-specific env injection/cleanup.
|
|
745
|
+
// Declared before try/finally so signal handlers and finally block can access it.
|
|
746
|
+
const adapterId = options.adapter || "claude-agent-acp";
|
|
747
|
+
// Everything after session creation is wrapped in try/finally to guarantee
|
|
748
|
+
// budget cleanup even if pre-loop setup (event logging, signal handlers) throws.
|
|
749
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
918
750
|
let consecutiveFailures = 0;
|
|
919
751
|
let agent = null;
|
|
920
752
|
let acpSessionId = null;
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
753
|
+
let exitReason = null;
|
|
754
|
+
let lastIterationCtx = null;
|
|
755
|
+
let lastErrorMessage;
|
|
756
|
+
let previousEnvValue; // For restoring pre-existing KSPEC_SESSION_ID
|
|
757
|
+
const recentTaskRefs = [];
|
|
758
|
+
const sessionIterationMap = new Map();
|
|
759
|
+
// Signal handler refs — declared here so finally can remove them
|
|
760
|
+
// AC: @ralph-task-limit ac-signal-cleanup
|
|
925
761
|
const signalCleanup = (signal) => {
|
|
926
762
|
info(`Received ${signal}, cleaning up...`);
|
|
927
|
-
// Kill agent if running
|
|
928
763
|
if (agent) {
|
|
929
764
|
agent.kill();
|
|
930
765
|
}
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
766
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
767
|
+
// Must use async IIFE — signal handlers are called synchronously,
|
|
768
|
+
// but cleanup needs async I/O. The IIFE keeps the event loop alive
|
|
769
|
+
// until cleanup completes, then exits explicitly.
|
|
770
|
+
void (async () => {
|
|
771
|
+
try {
|
|
772
|
+
await Promise.all([
|
|
773
|
+
fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { }),
|
|
774
|
+
closeSession(specDir, sessionId, "abandoned", `Received ${signal}`),
|
|
775
|
+
removeEnvForAdapter(adapterId, previousEnvValue),
|
|
776
|
+
]);
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
// Best-effort cleanup — don't let errors prevent exit
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
process.exit(0);
|
|
783
|
+
}
|
|
784
|
+
})();
|
|
938
785
|
};
|
|
939
786
|
const sigintHandler = () => { signalCleanup("SIGINT"); };
|
|
940
787
|
const sigtermHandler = () => { signalCleanup("SIGTERM"); };
|
|
941
|
-
process.on("SIGINT", sigintHandler);
|
|
942
|
-
process.on("SIGTERM", sigtermHandler);
|
|
943
|
-
// Create translator and renderer for this session
|
|
944
|
-
const translator = createTranslator();
|
|
945
|
-
const renderer = createCliRenderer();
|
|
946
|
-
// Task limit state - tracks completions per iteration
|
|
947
|
-
// AC: @ralph-task-limit ac-reset, ac-wrapup
|
|
948
|
-
let taskLimitReached = false;
|
|
949
|
-
let tasksCompletedThisIteration = 0;
|
|
950
|
-
// End-loop signal state
|
|
951
|
-
// AC: @ralph-end-loop ac-detect, ac-graceful
|
|
952
|
-
let endLoopRequested = false;
|
|
953
|
-
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1 - Track exit reason for wrap-up
|
|
954
|
-
let exitReason = null;
|
|
955
|
-
let lastIterationCtx = null;
|
|
956
|
-
let lastErrorMessage;
|
|
957
|
-
const recentTaskRefs = [];
|
|
958
|
-
// Map ACP session IDs to their iteration number.
|
|
959
|
-
// The agent's "update" handler persists across iterations and receives
|
|
960
|
-
// the ACP session ID (_sid) on each event. By looking up the iteration
|
|
961
|
-
// from this map, late updates from a previous ACP session are correctly
|
|
962
|
-
// attributed even after the loop has advanced to the next iteration.
|
|
963
|
-
const sessionIterationMap = new Map();
|
|
964
788
|
try {
|
|
789
|
+
// AC: @session-end-loop-signal ac-session-close-signal
|
|
790
|
+
// Install signal handlers FIRST, before any async work, so signals
|
|
791
|
+
// during startup (e.g. during appendEvent) still trigger cleanup.
|
|
792
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
793
|
+
process.on("SIGINT", sigintHandler);
|
|
794
|
+
process.on("SIGTERM", sigtermHandler);
|
|
795
|
+
// Inject KSPEC_SESSION_ID into agent harness config so it reaches child
|
|
796
|
+
// processes. Inside try/finally so cleanup runs even if injection fails.
|
|
797
|
+
// Process env alone is insufficient — some harnesses (e.g., Claude Code)
|
|
798
|
+
// sandbox child processes and don't forward arbitrary parent env vars.
|
|
799
|
+
// AC: @ralph-session-budget-integration ac-env-inject
|
|
800
|
+
const injectionResult = await injectEnvForAdapter(adapterId, sessionId);
|
|
801
|
+
previousEnvValue = injectionResult?.previousValue;
|
|
802
|
+
// Log session start
|
|
803
|
+
await appendEvent(specDir, {
|
|
804
|
+
session_id: sessionId,
|
|
805
|
+
type: "session.start",
|
|
806
|
+
data: {
|
|
807
|
+
adapter: options.adapter,
|
|
808
|
+
maxLoops,
|
|
809
|
+
maxRetries,
|
|
810
|
+
maxFailures,
|
|
811
|
+
maxTasks,
|
|
812
|
+
yolo: options.yolo,
|
|
813
|
+
focus: options.focus,
|
|
814
|
+
explicitTasks: explicitTaskScope?.refs,
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
// Create translator and renderer for this session
|
|
818
|
+
const translator = createTranslator();
|
|
819
|
+
const renderer = createCliRenderer();
|
|
965
820
|
for (let iteration = 1; iteration <= maxLoops; iteration++) {
|
|
966
821
|
renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
|
|
967
|
-
// AC: @ralph-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
// AC: @ralph-end-loop ac-cleanup - Reset end-loop state
|
|
977
|
-
endLoopRequested = false;
|
|
978
|
-
const wasStaleEndLoop = await clearStaleEndLoopMarker(ctx.rootDir);
|
|
979
|
-
if (wasStaleEndLoop) {
|
|
980
|
-
info("Cleared stale end-loop marker from previous session");
|
|
822
|
+
// AC: @ralph-session-budget-integration ac-reset-iteration
|
|
823
|
+
// Reset budget counter at iteration start (no-op when no budget exists)
|
|
824
|
+
await resetBudget(specDir, sessionId);
|
|
825
|
+
// AC: @session-end-loop-signal ac-detect - Check session state for end-loop
|
|
826
|
+
const endLoopState = await isEndLoopRequested(specDir, sessionId);
|
|
827
|
+
if (endLoopState?.requested) {
|
|
828
|
+
info(`End-loop already requested for this session. Exiting.`);
|
|
829
|
+
exitReason = "end_loop_signal";
|
|
830
|
+
break;
|
|
981
831
|
}
|
|
982
|
-
await clearEndLoopMarker(ctx.rootDir);
|
|
983
832
|
// Gather fresh context each iteration
|
|
984
833
|
// AC: @cli-ralph ac-16 - Only automation-eligible tasks (unless explicit scope)
|
|
985
834
|
// AC: @cli-ralph ac-21 - With explicit task scope, ignore automation eligibility
|
|
@@ -1055,7 +904,7 @@ export function registerRalphCommand(program) {
|
|
|
1055
904
|
// AC: @cli-ralph ac-21 - Include explicit task scope in prompt
|
|
1056
905
|
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, ctx.config.ralph.skills.task_work, options.focus, explicitTaskScope);
|
|
1057
906
|
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId, ctx.config.ralph.skills.reflect);
|
|
1058
|
-
// AC: @
|
|
907
|
+
// AC: @cli-ralph ac-21
|
|
1059
908
|
if (options.dryRun) {
|
|
1060
909
|
console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
|
|
1061
910
|
console.log(` max-loops: ${maxLoops}`);
|
|
@@ -1098,8 +947,10 @@ export function registerRalphCommand(program) {
|
|
|
1098
947
|
// Spawn agent if not already running
|
|
1099
948
|
if (!agent) {
|
|
1100
949
|
info("Spawning ACP agent...");
|
|
950
|
+
// AC: @ralph-session-budget-integration ac-env-inject
|
|
1101
951
|
agent = await spawnAndInitialize(adapter, {
|
|
1102
952
|
cwd: process.cwd(),
|
|
953
|
+
env: { KSPEC_SESSION_ID: sessionId },
|
|
1103
954
|
clientOptions: {
|
|
1104
955
|
clientInfo: {
|
|
1105
956
|
name: "kspec-ralph",
|
|
@@ -1118,64 +969,6 @@ export function registerRalphCommand(program) {
|
|
|
1118
969
|
if (event) {
|
|
1119
970
|
renderer.render(event);
|
|
1120
971
|
}
|
|
1121
|
-
// AC: @ralph-task-limit ac-detection, ac-wrapup
|
|
1122
|
-
// Detect task completions for limit enforcement
|
|
1123
|
-
if (maxTasks > 0 && !taskLimitReached) {
|
|
1124
|
-
const bashCmd = extractBashCommand(update);
|
|
1125
|
-
if (bashCmd && detectTaskCompleteCommand(bashCmd)) {
|
|
1126
|
-
// Pattern matched - verify via kspec query
|
|
1127
|
-
getIterationStats(ctx, iterationStartTime)
|
|
1128
|
-
.then(async (stats) => {
|
|
1129
|
-
if (stats.tasks_completed >= maxTasks && !taskLimitReached) {
|
|
1130
|
-
taskLimitReached = true;
|
|
1131
|
-
tasksCompletedThisIteration = stats.tasks_completed;
|
|
1132
|
-
info(`Task limit reached (${stats.tasks_completed}/${maxTasks})`);
|
|
1133
|
-
// AC: @ralph-task-limit ac-marker-format, ac-wrapup
|
|
1134
|
-
// Write marker file for hook enforcement
|
|
1135
|
-
const marker = {
|
|
1136
|
-
active: true,
|
|
1137
|
-
since: iterationStartTime.toISOString(),
|
|
1138
|
-
max: maxTasks,
|
|
1139
|
-
completed: stats.tasks_completed,
|
|
1140
|
-
sessionId,
|
|
1141
|
-
};
|
|
1142
|
-
await writeTaskLimitMarker(ctx.rootDir, marker);
|
|
1143
|
-
// Inject wrap-up message to agent
|
|
1144
|
-
if (agent && acpSessionId) {
|
|
1145
|
-
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(", ")}`;
|
|
1146
|
-
agent.client.prompt({
|
|
1147
|
-
sessionId: acpSessionId,
|
|
1148
|
-
prompt: [{ type: "text", text: wrapUpMsg }],
|
|
1149
|
-
}).catch(() => {
|
|
1150
|
-
// Ignore if message injection fails
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
})
|
|
1155
|
-
.catch(() => {
|
|
1156
|
-
// Ignore query failures - detection is best-effort
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
// AC: @ralph-end-loop ac-detect
|
|
1161
|
-
// Detect explicit end-loop command
|
|
1162
|
-
if (!endLoopRequested) {
|
|
1163
|
-
const bashCmd = extractBashCommand(update);
|
|
1164
|
-
if (bashCmd && detectEndLoopCommand(bashCmd)) {
|
|
1165
|
-
endLoopRequested = true;
|
|
1166
|
-
// Read marker to get reason if present
|
|
1167
|
-
readEndLoopMarker(ctx.rootDir)
|
|
1168
|
-
.then((marker) => {
|
|
1169
|
-
const reason = marker?.reason
|
|
1170
|
-
? ` (${marker.reason})`
|
|
1171
|
-
: "";
|
|
1172
|
-
info(`End-loop signal received${reason}`);
|
|
1173
|
-
})
|
|
1174
|
-
.catch(() => {
|
|
1175
|
-
info("End-loop signal received");
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
972
|
// Log raw update event (async, non-blocking)
|
|
1180
973
|
// Look up iteration by ACP session ID so late updates from
|
|
1181
974
|
// a previous session are attributed to the correct iteration
|
|
@@ -1280,12 +1073,6 @@ export function registerRalphCommand(program) {
|
|
|
1280
1073
|
}
|
|
1281
1074
|
}
|
|
1282
1075
|
lastIterationCtx = sessionCtx;
|
|
1283
|
-
// AC: @ralph-end-loop ac-graceful - Check for end-loop signal
|
|
1284
|
-
if (endLoopRequested) {
|
|
1285
|
-
info("Agent requested end of loop. Exiting gracefully.");
|
|
1286
|
-
exitReason = "end_loop_signal";
|
|
1287
|
-
break;
|
|
1288
|
-
}
|
|
1289
1076
|
// Periodic agent restart to prevent OOM
|
|
1290
1077
|
// AC: @cli-ralph ac-restart-periodic
|
|
1291
1078
|
if (restartEvery > 0 &&
|
|
@@ -1323,6 +1110,13 @@ export function registerRalphCommand(program) {
|
|
|
1323
1110
|
exitReason = "max_iterations";
|
|
1324
1111
|
}
|
|
1325
1112
|
}
|
|
1113
|
+
catch (loopErr) {
|
|
1114
|
+
// AC: @session-end-loop-signal ac-session-close-error
|
|
1115
|
+
// Unrecoverable error during loop execution
|
|
1116
|
+
exitReason = exitReason ?? "error";
|
|
1117
|
+
lastErrorMessage = loopErr.message;
|
|
1118
|
+
error("Unrecoverable error in ralph loop", loopErr);
|
|
1119
|
+
}
|
|
1326
1120
|
finally {
|
|
1327
1121
|
// Remove signal handlers to avoid double cleanup
|
|
1328
1122
|
process.off("SIGINT", sigintHandler);
|
|
@@ -1332,10 +1126,13 @@ export function registerRalphCommand(program) {
|
|
|
1332
1126
|
agent.kill();
|
|
1333
1127
|
agent = null;
|
|
1334
1128
|
}
|
|
1335
|
-
// AC: @ralph-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
await
|
|
1129
|
+
// AC: @ralph-session-budget-integration ac-session-close-all-paths
|
|
1130
|
+
// Clean up budget file and harness env injection when session ends
|
|
1131
|
+
await fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { });
|
|
1132
|
+
await removeEnvForAdapter(adapterId, previousEnvValue);
|
|
1133
|
+
// Clean up session env vars
|
|
1134
|
+
delete process.env.KSPEC_RALPH_SESSION;
|
|
1135
|
+
delete process.env.KSPEC_SESSION_ID;
|
|
1339
1136
|
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1, ac-2, ac-3, ac-4, ac-5
|
|
1340
1137
|
// Spawn wrap-up agent if not dry-run and we have an exit reason
|
|
1341
1138
|
if (!options.dryRun && exitReason) {
|
|
@@ -1382,8 +1179,25 @@ export function registerRalphCommand(program) {
|
|
|
1382
1179
|
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1383
1180
|
console.log("");
|
|
1384
1181
|
}
|
|
1385
|
-
// Log session end
|
|
1386
|
-
|
|
1182
|
+
// Log session end and close session with appropriate status/reason
|
|
1183
|
+
// AC: @session-end-loop-signal ac-session-close-normal, ac-session-close-error
|
|
1184
|
+
const isErrorExit = consecutiveFailures >= maxFailures ||
|
|
1185
|
+
exitReason === "max_failures" ||
|
|
1186
|
+
exitReason === "error";
|
|
1187
|
+
const status = isErrorExit ? "abandoned" : "completed";
|
|
1188
|
+
const closeReason = exitReason === "max_failures"
|
|
1189
|
+
? `Max failures reached (${consecutiveFailures}/${maxFailures})${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
|
|
1190
|
+
: exitReason === "error"
|
|
1191
|
+
? `Unrecoverable error${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
|
|
1192
|
+
: exitReason === "end_loop_signal"
|
|
1193
|
+
? "Agent requested end of loop"
|
|
1194
|
+
: exitReason === "max_iterations"
|
|
1195
|
+
? `Completed all ${maxLoops} iterations`
|
|
1196
|
+
: exitReason === "no_tasks"
|
|
1197
|
+
? "No eligible tasks remaining"
|
|
1198
|
+
: exitReason === "explicit_tasks_done"
|
|
1199
|
+
? "All explicit tasks completed"
|
|
1200
|
+
: `Loop ended: ${exitReason}`;
|
|
1387
1201
|
await appendEvent(specDir, {
|
|
1388
1202
|
session_id: sessionId,
|
|
1389
1203
|
type: "session.end",
|
|
@@ -1391,9 +1205,10 @@ export function registerRalphCommand(program) {
|
|
|
1391
1205
|
status,
|
|
1392
1206
|
consecutiveFailures,
|
|
1393
1207
|
exitReason,
|
|
1208
|
+
closeReason,
|
|
1394
1209
|
},
|
|
1395
1210
|
});
|
|
1396
|
-
await
|
|
1211
|
+
await closeSession(specDir, sessionId, status, closeReason);
|
|
1397
1212
|
}
|
|
1398
1213
|
console.log(chalk.green(`\n${"─".repeat(60)}`));
|
|
1399
1214
|
success("Ralph loop completed");
|