ralph-flow-pi 0.1.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/LICENSE +21 -0
- package/README.md +428 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/prompts.d.ts +25 -0
- package/dist/commands/prompts.d.ts.map +1 -0
- package/dist/commands/prompts.js +249 -0
- package/dist/commands/prompts.js.map +1 -0
- package/dist/commands/tools.d.ts +47 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +633 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/engine/check-bash.d.ts +121 -0
- package/dist/engine/check-bash.d.ts.map +1 -0
- package/dist/engine/check-bash.js +373 -0
- package/dist/engine/check-bash.js.map +1 -0
- package/dist/engine/check.d.ts +47 -0
- package/dist/engine/check.d.ts.map +1 -0
- package/dist/engine/check.js +298 -0
- package/dist/engine/check.js.map +1 -0
- package/dist/engine/core.d.ts +153 -0
- package/dist/engine/core.d.ts.map +1 -0
- package/dist/engine/core.js +1984 -0
- package/dist/engine/core.js.map +1 -0
- package/dist/engine/lock.d.ts +27 -0
- package/dist/engine/lock.d.ts.map +1 -0
- package/dist/engine/lock.js +121 -0
- package/dist/engine/lock.js.map +1 -0
- package/dist/engine/runner.d.ts +108 -0
- package/dist/engine/runner.d.ts.map +1 -0
- package/dist/engine/runner.js +510 -0
- package/dist/engine/runner.js.map +1 -0
- package/dist/engine/skills.d.ts +53 -0
- package/dist/engine/skills.d.ts.map +1 -0
- package/dist/engine/skills.js +109 -0
- package/dist/engine/skills.js.map +1 -0
- package/dist/engine/step-tools.d.ts +22 -0
- package/dist/engine/step-tools.d.ts.map +1 -0
- package/dist/engine/step-tools.js +45 -0
- package/dist/engine/step-tools.js.map +1 -0
- package/dist/engine/types.d.ts +136 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +20 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/headless.d.ts +57 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +318 -0
- package/dist/headless.js.map +1 -0
- package/dist/pi/adapter.d.ts +135 -0
- package/dist/pi/adapter.d.ts.map +1 -0
- package/dist/pi/adapter.js +231 -0
- package/dist/pi/adapter.js.map +1 -0
- package/dist/pi/interactive.d.ts +28 -0
- package/dist/pi/interactive.d.ts.map +1 -0
- package/dist/pi/interactive.js +58 -0
- package/dist/pi/interactive.js.map +1 -0
- package/dist/pi/tui.d.ts +12 -0
- package/dist/pi/tui.d.ts.map +1 -0
- package/dist/pi/tui.js +12 -0
- package/dist/pi/tui.js.map +1 -0
- package/dist/tui/app.d.ts +25 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +47 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/embed.d.ts +42 -0
- package/dist/tui/embed.d.ts.map +1 -0
- package/dist/tui/embed.js +38 -0
- package/dist/tui/embed.js.map +1 -0
- package/dist/tui/extension.d.ts +88 -0
- package/dist/tui/extension.d.ts.map +1 -0
- package/dist/tui/extension.js +114 -0
- package/dist/tui/extension.js.map +1 -0
- package/dist/tui/history-editor.d.ts +38 -0
- package/dist/tui/history-editor.d.ts.map +1 -0
- package/dist/tui/history-editor.js +55 -0
- package/dist/tui/history-editor.js.map +1 -0
- package/dist/tui/launcher.d.ts +24 -0
- package/dist/tui/launcher.d.ts.map +1 -0
- package/dist/tui/launcher.js +97 -0
- package/dist/tui/launcher.js.map +1 -0
- package/dist/tui/render.d.ts +87 -0
- package/dist/tui/render.d.ts.map +1 -0
- package/dist/tui/render.js +266 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/run-app.d.ts +49 -0
- package/dist/tui/run-app.d.ts.map +1 -0
- package/dist/tui/run-app.js +317 -0
- package/dist/tui/run-app.js.map +1 -0
- package/dist/tui/run-model.d.ts +162 -0
- package/dist/tui/run-model.d.ts.map +1 -0
- package/dist/tui/run-model.js +280 -0
- package/dist/tui/run-model.js.map +1 -0
- package/dist/tui/run-view.d.ts +71 -0
- package/dist/tui/run-view.d.ts.map +1 -0
- package/dist/tui/run-view.js +167 -0
- package/dist/tui/run-view.js.map +1 -0
- package/dist/tui/welcome-header.d.ts +40 -0
- package/dist/tui/welcome-header.d.ts.map +1 -0
- package/dist/tui/welcome-header.js +90 -0
- package/dist/tui/welcome-header.js.map +1 -0
- package/package.json +55 -0
- package/skills/c-to-rust-audit/SKILL.md +67 -0
- package/skills/c-to-rust-implement/SKILL.md +151 -0
- package/skills/c-to-rust-implement/references/c-to-rust-patterns.md +86 -0
- package/skills/c-to-rust-implement/references/conditional-compilation.md +47 -0
- package/skills/c-to-rust-implement/references/crate-reference.md +15 -0
- package/skills/c-to-rust-implement/references/error-strategies.md +80 -0
- package/skills/c-to-rust-implement/references/inline-asm.md +37 -0
- package/skills/c-to-rust-plan/SKILL.md +166 -0
- package/skills/c-to-rust-plan/references/detection-commands.md +66 -0
- package/skills/c-to-rust-test-gen/SKILL.md +130 -0
- package/skills/c-to-rust-test-gen/references/proptest-patterns.md +81 -0
- package/skills/c-to-rust-test-gen/references/test-porting.md +56 -0
- package/skills/c-to-rust-validate/SKILL.md +121 -0
- package/skills/everything2rust-audit/SKILL.md +69 -0
- package/skills/everything2rust-design/SKILL.md +121 -0
- package/skills/everything2rust-design/references/domain-playbooks.md +68 -0
- package/skills/everything2rust-design/references/paradigm-map.md +99 -0
- package/skills/everything2rust-implement/SKILL.md +101 -0
- package/skills/everything2rust-spec/SKILL.md +86 -0
- package/skills/everything2rust-spec/references/oracle-strategies.md +96 -0
- package/skills/everything2rust-survey/SKILL.md +99 -0
- package/skills/everything2rust-test-gen/SKILL.md +68 -0
- package/skills/everything2rust-test-gen/references/harness-patterns.md +186 -0
- package/skills/everything2rust-validate/SKILL.md +85 -0
- package/workflows/c-to-rust.yaml +202 -0
- package/workflows/everything2rust.yaml +259 -0
- package/workflows/loop.yaml +68 -0
- package/workflows/spec.yaml +183 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six ralphflow_* tools, exposed to the main chat session.
|
|
3
|
+
*
|
|
4
|
+
* Names and response texts are a verbatim port of the plugin versions' tools, so
|
|
5
|
+
* the workflow YAML, the docs and the user's muscle memory all carry over.
|
|
6
|
+
*
|
|
7
|
+
* Two runtime differences (SYNC.md has the full list):
|
|
8
|
+
*
|
|
9
|
+
* - The plugins ran the CHECK from an idle event, so ralphflow_continue was
|
|
10
|
+
* "pure state management" that promised verification would happen "when you
|
|
11
|
+
* go idle". Here the runner owns execution: continue mutates the state and
|
|
12
|
+
* then calls ensureRunning, and the loop does the rest. The user-facing texts
|
|
13
|
+
* say "自动" either way; only the mechanism moved.
|
|
14
|
+
* - opencode needed no lock (one plugin process per project). A `ralph` CLI is
|
|
15
|
+
* an ordinary process, so every state mutation takes the cross-process
|
|
16
|
+
* instance lock, and start/continue refuse an instance another live process
|
|
17
|
+
* is already driving.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import os from "os";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { Type } from "typebox";
|
|
23
|
+
import { DONE_REPORTED_MARKER, MANUAL_GATE_MARKER, MAX_NESTING_DEPTH } from "../engine/core.js";
|
|
24
|
+
import { hasActiveCheck } from "../engine/check.js";
|
|
25
|
+
import { isInstanceGone, withInstanceLock } from "../engine/lock.js";
|
|
26
|
+
import { formatSkillReport, loadSkillIndex } from "../engine/skills.js";
|
|
27
|
+
import { isSubWorkflowStep } from "../engine/types.js";
|
|
28
|
+
import { defineTool } from "../pi/adapter.js";
|
|
29
|
+
import { attachRunView } from "../tui/embed.js";
|
|
30
|
+
const text = (t) => ({ content: [{ type: "text", text: t }], details: {} });
|
|
31
|
+
/**
|
|
32
|
+
* `terminate: true` (AgentToolResult, pi-agent-core) tells pi's agent loop to
|
|
33
|
+
* stop after this tool batch instead of invoking the model again. Set on a
|
|
34
|
+
* detach so the turn ends there and then — the same effect the user
|
|
35
|
+
* confirmed by hand (Esc the run view, then Esc again in chat to abort the
|
|
36
|
+
* agent's turn): this makes that automatic, one Esc instead of two.
|
|
37
|
+
*/
|
|
38
|
+
const terminateText = (t) => ({ content: [{ type: "text", text: t }], details: {}, terminate: true });
|
|
39
|
+
/**
|
|
40
|
+
* Appended after ralphflow_start/continue/watch attach the run view and it
|
|
41
|
+
* later gives control back — either the workflow reached a terminal state,
|
|
42
|
+
* or the user detached (Esc on an empty input) while it was still going.
|
|
43
|
+
* Names the exact way back in (the /ralphflow-watch command, or just asking)
|
|
44
|
+
* so the answer to "how do I check on it" is right there, not something the
|
|
45
|
+
* user has to already know.
|
|
46
|
+
*/
|
|
47
|
+
function describeAttachOutcome(result) {
|
|
48
|
+
if (result.outcome === "completed") {
|
|
49
|
+
return `\n\n---\n\n## 工作流完成 ✓${result.reportPath ? `\n\n执行报告:${result.reportPath}` : ""}`;
|
|
50
|
+
}
|
|
51
|
+
if (result.outcome === "cancelled") {
|
|
52
|
+
return "\n\n---\n\n工作流已取消。";
|
|
53
|
+
}
|
|
54
|
+
return "\n\n---\n\n已切回聊天。工作流在后台继续运行。想再看实时进度:直接说\"看着它跑\",或者输入 `/ralphflow-watch`。不需要的话不用管它——它需要人工审查、遇到暂停,或者跑完时,会自动出现在这个对话里。";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `terminate: true` alone was NOT enough — confirmed against a real model in
|
|
58
|
+
* a real terminal (deepseek-v4-flash, 2026-07-18): after a detach, it was
|
|
59
|
+
* invoked again anyway, said "The workflow is running in the background. Let
|
|
60
|
+
* me watch it to see the progress," and called ralphflow_watch. Pi's own doc
|
|
61
|
+
* comment calls `terminate` a "hint" ("Early termination only happens when
|
|
62
|
+
* EVERY finalized tool result in the batch sets this to true") — if the model
|
|
63
|
+
* called anything else alongside ralphflow_start in the same turn, one
|
|
64
|
+
* non-terminating result anywhere in that batch silently defeats it for the
|
|
65
|
+
* whole turn. When that happened here, the model didn't just re-call
|
|
66
|
+
* ralphflow_watch — it read the DO step's own tool-call log line in the
|
|
67
|
+
* transcript (posted by runner.ts's permanent onStepEvent listener, e.g.
|
|
68
|
+
* "▸ [create] bash mkdir -p ...") as if addressed to itself, and started
|
|
69
|
+
* executing that command with its OWN bash tool — a second agent racing the
|
|
70
|
+
* real DO session on the same files.
|
|
71
|
+
*
|
|
72
|
+
* `ExtensionContext.abort()` — the same "abort the current agent operation"
|
|
73
|
+
* primitive a manual Esc-in-chat triggers, per the user's own working manual
|
|
74
|
+
* test — is what actually stopped it: unconditional, doesn't depend on every
|
|
75
|
+
* tool in a batch cooperating. `setActiveTools`/`getActiveTools` were tried
|
|
76
|
+
* as a second layer here first, but don't actually exist on Pi 0.80.10's real
|
|
77
|
+
* interactive-mode UI context despite being declared in ExtensionUIContext's
|
|
78
|
+
* types — confirmed by grepping the compiled interactive-mode.js for a real
|
|
79
|
+
* implementation (none) and by a real run throwing "ctx.ui.getActiveTools is
|
|
80
|
+
* not a function" from the "input" handler that tried to use it. Removed
|
|
81
|
+
* rather than left in as a guarded no-op: dead code that silently does
|
|
82
|
+
* nothing is worse than no code, because it reads as a working second layer
|
|
83
|
+
* of defense when it is not one.
|
|
84
|
+
*/
|
|
85
|
+
function attachResult(prefix, result, ctx) {
|
|
86
|
+
const body = prefix + describeAttachOutcome(result);
|
|
87
|
+
if (result.outcome !== "detached")
|
|
88
|
+
return text(body);
|
|
89
|
+
ctx.abort?.();
|
|
90
|
+
return terminateText(body);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The state-machine core of "continue": approve a manual review / resume a
|
|
94
|
+
* paused workflow / recover from a crash / attach to an instance interrupted
|
|
95
|
+
* mid-DO. Pulled out of the `ralphflow_continue` tool so `ralphflow continue`
|
|
96
|
+
* (the headless CLI verb in headless.ts) can drive the exact same six
|
|
97
|
+
* branches without a chat session or a run-view attach — headless has no
|
|
98
|
+
* `ctx.ui.custom()` to borrow a terminal from, and doesn't need one: this
|
|
99
|
+
* function only mutates state and returns what happened, it never touches
|
|
100
|
+
* the runner or the UI. Caller is responsible for the instance lock (both
|
|
101
|
+
* call sites already need `withInstanceLock` for other reasons) and for
|
|
102
|
+
* calling `runner.ensureRunning(instId)` afterward.
|
|
103
|
+
*
|
|
104
|
+
* `attached`: true when the caller does NOT already own this instance's
|
|
105
|
+
* session — this is what makes branch 5 (crash-recovery re-attach) reachable.
|
|
106
|
+
* Headless callers always pass true: a one-shot CLI invocation has no
|
|
107
|
+
* standing session identity to already "be" the owner of anything.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveContinueAction(engine, instId, sessionId, attached) {
|
|
110
|
+
engine.bindInstance(instId, sessionId);
|
|
111
|
+
let state = engine.readState(instId);
|
|
112
|
+
if (!state || !state.active) {
|
|
113
|
+
return "没有活跃的工作流。使用 ralphflow_start 启动一个。";
|
|
114
|
+
}
|
|
115
|
+
const workflow = engine.loadWorkflow(state.workflow_name);
|
|
116
|
+
if (!workflow) {
|
|
117
|
+
return `工作流 "${state.workflow_name}" 未找到。`;
|
|
118
|
+
}
|
|
119
|
+
// 1. check_error pause: the verifier couldn't run. Reset to check phase
|
|
120
|
+
// (the work is untouched) — the runner re-verifies immediately.
|
|
121
|
+
if (state.paused && state.pause_reason === "check_error" && state.current_phase === "check") {
|
|
122
|
+
const step = engine.getStep(workflow, state.current_step);
|
|
123
|
+
if (step && !isSubWorkflowStep(step)) {
|
|
124
|
+
engine.writeState({ ...state, paused: false, pause_reason: undefined }, instId);
|
|
125
|
+
engine.logEvent(instId, "info", "check_retry_after_infra_error", { workflow: state.workflow_name, step: step.id });
|
|
126
|
+
return "验证基础设施故障已清除,工作流恢复。正在重新验证。";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 2. manual gate: the user approves the work. Clearing the gate IS the
|
|
130
|
+
// approval — the runner then verifies. Do NOT touch the done-reported
|
|
131
|
+
// marker: the runner needs it to know the DO phase is finished.
|
|
132
|
+
if (engine.markerExists(MANUAL_GATE_MARKER, instId)) {
|
|
133
|
+
engine.clearManualStepMarker(instId);
|
|
134
|
+
engine.clearManualGate(instId);
|
|
135
|
+
engine.logEvent(instId, "info", "manual_gate_approved", { step: state.current_step });
|
|
136
|
+
return `## ✅ 审查通过\n\n步骤 \`${state.current_step}\` 已批准。正在运行独立验证。`;
|
|
137
|
+
}
|
|
138
|
+
// 3. Paused (max_failures / config_error / session_*): reset fail_count
|
|
139
|
+
// and re-issue the DO prompt for a fresh attempt.
|
|
140
|
+
if (state.paused) {
|
|
141
|
+
const previousFailCount = state.fail_count;
|
|
142
|
+
const previousReason = state.last_failure_reason;
|
|
143
|
+
engine.clearReinjectCounter(instId);
|
|
144
|
+
engine.clearDoneReported(instId);
|
|
145
|
+
engine.clearManualGate(instId);
|
|
146
|
+
engine.writeState({ ...state, current_phase: "do", paused: false, pause_reason: undefined, fail_count: 0 }, instId);
|
|
147
|
+
engine.logEvent(instId, "info", "workflow_resumed", { workflow: state.workflow_name, step: state.current_step });
|
|
148
|
+
const step = engine.getStep(workflow, state.current_step);
|
|
149
|
+
if (!step) {
|
|
150
|
+
return `已恢复。当前步骤:${state.current_step}`;
|
|
151
|
+
}
|
|
152
|
+
if (isSubWorkflowStep(step)) {
|
|
153
|
+
engine.recordStepStart(instId, step.id, "do");
|
|
154
|
+
engine.logEvent(instId, "info", "step_start", { step: step.id, phase: "do" });
|
|
155
|
+
// Peek the stack top before pushing our own return-address frame for
|
|
156
|
+
// this entry attempt. Two cases:
|
|
157
|
+
// - it already matches (workflow_name, current_step) exactly → an
|
|
158
|
+
// earlier attempt at this SAME entry (crashed or interrupted before
|
|
159
|
+
// it could resolve) left a duplicate frame; leave it popped so the
|
|
160
|
+
// push below replaces it instead of stacking a second copy.
|
|
161
|
+
// - it's anything else (an outer nesting level's real return address,
|
|
162
|
+
// or nothing) → not ours to touch; push it straight back before
|
|
163
|
+
// layering our own frame on top, so outer context survives.
|
|
164
|
+
const staleTop = engine.popState(instId);
|
|
165
|
+
const staleIsMismatch = staleTop && !(staleTop.workflow_name === state.workflow_name && staleTop.current_step === state.current_step);
|
|
166
|
+
if (staleIsMismatch) {
|
|
167
|
+
engine.pushState(staleTop, instId);
|
|
168
|
+
}
|
|
169
|
+
engine.pushState({ ...state, current_step: step.id, current_phase: "do", fail_count: 0, paused: false, pause_reason: undefined }, instId);
|
|
170
|
+
const subResult = engine.resolveSubWorkflowEntry(instId, step.workflow, state.user_task, step, MAX_NESTING_DEPTH, previousReason, previousFailCount);
|
|
171
|
+
if (subResult.error) {
|
|
172
|
+
engine.popState(instId); // undo our push
|
|
173
|
+
if (staleIsMismatch)
|
|
174
|
+
engine.popState(instId); // also undo the stale push-back
|
|
175
|
+
engine.writeState({ ...state, paused: true, pause_reason: "config_error", last_failure_reason: subResult.text }, instId);
|
|
176
|
+
return subResult.text;
|
|
177
|
+
}
|
|
178
|
+
let resumeMsg = `## 工作流已恢复\n\n之前尝试次数:${previousFailCount}`;
|
|
179
|
+
if (previousReason)
|
|
180
|
+
resumeMsg += `\n\n### 上次失败原因\n${previousReason}`;
|
|
181
|
+
resumeMsg += "\n\n---\n\n";
|
|
182
|
+
return resumeMsg + `重新进入子工作流:**${step.id}**`;
|
|
183
|
+
}
|
|
184
|
+
if (workflow.manual_step && workflow.manual_step.includes(step.id)) {
|
|
185
|
+
engine.writeManualStepMarker(instId);
|
|
186
|
+
}
|
|
187
|
+
engine.buildDoPrompt(instId, step, state.user_task, previousReason, previousFailCount);
|
|
188
|
+
let resumeMsg = `## 工作流已恢复\n\n之前尝试次数:${previousFailCount}`;
|
|
189
|
+
if (previousReason)
|
|
190
|
+
resumeMsg += `\n\n### 上次失败原因\n${previousReason}`;
|
|
191
|
+
return resumeMsg + `\n\n---\n\n重新执行步骤 **${step.id}** - ${step.desc}`;
|
|
192
|
+
}
|
|
193
|
+
// 4. Crash recovery: stuck in check phase with no active verifier →
|
|
194
|
+
// reset to do and re-run the step.
|
|
195
|
+
if (state.current_phase !== "do") {
|
|
196
|
+
if (state.current_phase === "check") {
|
|
197
|
+
if (hasActiveCheck(instId)) {
|
|
198
|
+
engine.logEvent(instId, "info", "crash_recovery_skipped", { step: state.current_step });
|
|
199
|
+
return `## ⏳ 验证进行中\n\n步骤 **${state.current_step}** 的对抗性检查仍在运行。\n\n请等待完成,或使用 \`/ralphflow-cancel\` 取消工作流。`;
|
|
200
|
+
}
|
|
201
|
+
engine.clearAdversarialSession(instId);
|
|
202
|
+
engine.logEvent(instId, "warn", "crash_recovery", { step: state.current_step });
|
|
203
|
+
state = { ...state, current_phase: "do" };
|
|
204
|
+
engine.writeState(state, instId);
|
|
205
|
+
engine.clearReinjectCounter(instId);
|
|
206
|
+
engine.clearManualStepMarker(instId);
|
|
207
|
+
engine.clearManualGate(instId);
|
|
208
|
+
engine.clearDoneReported(instId);
|
|
209
|
+
const step = engine.getStep(workflow, state.current_step);
|
|
210
|
+
if (!step) {
|
|
211
|
+
return `崩溃恢复:步骤 "${state.current_step}" 在工作流中未找到。`;
|
|
212
|
+
}
|
|
213
|
+
if (isSubWorkflowStep(step)) {
|
|
214
|
+
// Same peek-and-dedup as the paused-resume branch above: replace a
|
|
215
|
+
// duplicate frame left by an earlier crashed attempt at this exact
|
|
216
|
+
// entry, but leave any genuinely different (outer) frame untouched.
|
|
217
|
+
const staleTop = engine.popState(instId);
|
|
218
|
+
const staleIsMismatch = staleTop && !(staleTop.workflow_name === state.workflow_name && staleTop.current_step === state.current_step);
|
|
219
|
+
if (staleIsMismatch) {
|
|
220
|
+
engine.pushState(staleTop, instId);
|
|
221
|
+
}
|
|
222
|
+
engine.pushState({ ...state, current_step: step.id, current_phase: "do", fail_count: state.fail_count || 0 }, instId);
|
|
223
|
+
const subResult = engine.resolveSubWorkflowEntry(instId, step.workflow, state.user_task, step, MAX_NESTING_DEPTH, "之前的验证被中断(进程崩溃)。请重新执行任务。", state.fail_count || 0);
|
|
224
|
+
if (subResult.error) {
|
|
225
|
+
engine.popState(instId); // undo our push
|
|
226
|
+
if (staleIsMismatch)
|
|
227
|
+
engine.popState(instId); // also undo the stale push-back
|
|
228
|
+
engine.writeState({ ...state, paused: true, pause_reason: "config_error", last_failure_reason: subResult.text }, instId);
|
|
229
|
+
return subResult.text;
|
|
230
|
+
}
|
|
231
|
+
return `## ⚠️ 崩溃恢复\n\n进程在验证期间崩溃。\n\n---\n\n重新进入子工作流:**${step.id}**`;
|
|
232
|
+
}
|
|
233
|
+
if (workflow.manual_step && workflow.manual_step.includes(step.id)) {
|
|
234
|
+
engine.writeManualStepMarker(instId);
|
|
235
|
+
}
|
|
236
|
+
engine.buildDoPrompt(instId, step, state.user_task, "之前的验证被中断(进程崩溃)。请重新执行任务。", state.fail_count || 0);
|
|
237
|
+
return `## ⚠️ 崩溃恢复\n\n进程在验证期间崩溃。DO 阶段已重置。\n\n---\n\n重新执行步骤 **${step.id}** - ${step.desc}`;
|
|
238
|
+
}
|
|
239
|
+
return `当前阶段是 "${state.current_phase}",不是 "do"。工作流已在处理中。`;
|
|
240
|
+
}
|
|
241
|
+
const step = engine.getStep(workflow, state.current_step);
|
|
242
|
+
if (!step)
|
|
243
|
+
return `步骤 "${state.current_step}" 未找到。`;
|
|
244
|
+
// 5. Attach: taking over an instance that died MID-DO (nothing reported,
|
|
245
|
+
// no manual gate). Re-issue the DO prompt.
|
|
246
|
+
if (attached && !engine.markerExists(DONE_REPORTED_MARKER, instId) && !engine.markerExists(MANUAL_GATE_MARKER, instId)) {
|
|
247
|
+
if (isSubWorkflowStep(step)) {
|
|
248
|
+
engine.pushState({ ...state, current_step: step.id, current_phase: "do", fail_count: state.fail_count || 0 }, instId);
|
|
249
|
+
const subResult = engine.resolveSubWorkflowEntry(instId, step.workflow, state.user_task, step, MAX_NESTING_DEPTH, state.last_failure_reason, state.fail_count || 0);
|
|
250
|
+
if (subResult.error) {
|
|
251
|
+
engine.popState(instId);
|
|
252
|
+
engine.writeState({ ...state, paused: true, pause_reason: "config_error", last_failure_reason: subResult.text }, instId);
|
|
253
|
+
return subResult.text;
|
|
254
|
+
}
|
|
255
|
+
engine.logEvent(instId, "info", "instance_attached_resume_do", { instance: instId, step: step.id });
|
|
256
|
+
return `## 已接管工作流实例 \`${instId}\`\n\n该实例中断于 DO 阶段,继续执行子工作流。\n\n---\n\n重新进入:**${step.id}**`;
|
|
257
|
+
}
|
|
258
|
+
if (workflow.manual_step && workflow.manual_step.includes(step.id)) {
|
|
259
|
+
engine.writeManualStepMarker(instId);
|
|
260
|
+
}
|
|
261
|
+
engine.buildDoPrompt(instId, step, state.user_task, state.last_failure_reason, state.fail_count || 0);
|
|
262
|
+
engine.logEvent(instId, "info", "instance_attached_resume_do", { instance: instId, step: step.id });
|
|
263
|
+
return `## 已接管工作流实例 \`${instId}\`\n\n该实例中断于 DO 阶段,继续执行当前步骤。\n\n---\n\n重新执行步骤 **${step.id}** - ${step.desc}`;
|
|
264
|
+
}
|
|
265
|
+
// 6. In do, no gate, no pause, no attach. The runner is already on it —
|
|
266
|
+
// or, if this process just started, ensureRunning below puts it back
|
|
267
|
+
// on the rails.
|
|
268
|
+
return "没有需要手动继续的操作。引擎会自动执行 DO 与验证阶段。";
|
|
269
|
+
}
|
|
270
|
+
export function createTools(ctx) {
|
|
271
|
+
const { engine, runner, getSessionId } = ctx;
|
|
272
|
+
/** Run a mutation under the instance lock, mapping a vanished instance to a message. */
|
|
273
|
+
async function locked(instId, fn) {
|
|
274
|
+
try {
|
|
275
|
+
return await withInstanceLock(engine.getInstanceDir(instId), instId, fn);
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
if (isInstanceGone(e))
|
|
279
|
+
return `实例 \`${instId}\` 已经不存在(可能刚被其他进程取消或完成)。`;
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Refuse to touch an instance another live process is driving. */
|
|
284
|
+
function foreignDriverNote(instId) {
|
|
285
|
+
const pid = engine.foreignRunnerPid(instId);
|
|
286
|
+
if (pid === null)
|
|
287
|
+
return null;
|
|
288
|
+
return `实例 \`${instId}\` 正被另一个 ralph 进程(pid ${pid})驱动。请在那个进程里操作它,或先结束该进程。`;
|
|
289
|
+
}
|
|
290
|
+
function stepsOverview(steps) {
|
|
291
|
+
return steps.map((s, i) => ` ${i + 1}. **${s.id}**: ${s.desc}${isSubWorkflowStep(s) ? ` (子工作流: ${s.workflow})` : ""}`).join("\n");
|
|
292
|
+
}
|
|
293
|
+
// ─── ralphflow_start ────────────────────────────────────────────────────────
|
|
294
|
+
const ralphflow_start = defineTool({
|
|
295
|
+
name: "ralphflow_start",
|
|
296
|
+
label: "Ralph Flow: Start",
|
|
297
|
+
description: "Start a workflow instance. Provide workflow name and task description. Multiple sessions can each run their own instance in the same project.",
|
|
298
|
+
parameters: Type.Object({
|
|
299
|
+
workflow: Type.String({ description: "Workflow name (use ralphflow_list to see available workflows)" }),
|
|
300
|
+
task: Type.String({ description: "Task description - what should be accomplished" }),
|
|
301
|
+
extra_dirs: Type.Optional(Type.Array(Type.String(), {
|
|
302
|
+
description: "Directories OUTSIDE the project that the task's source material lives in (absolute paths or ~/...). The independent CHECK verifier gets read access to them; each must exist or the start is refused.",
|
|
303
|
+
})),
|
|
304
|
+
}),
|
|
305
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
306
|
+
const sessionId = getSessionId();
|
|
307
|
+
const { workflow, task } = params;
|
|
308
|
+
const extra_dirs = params.extra_dirs ?? [];
|
|
309
|
+
// Refuse if this session already runs an active instance.
|
|
310
|
+
const instances = engine.listInstances();
|
|
311
|
+
const mine = instances.find((i) => i.owner === sessionId);
|
|
312
|
+
if (mine) {
|
|
313
|
+
return text(`当前会话已有活跃工作流实例 \`${mine.id}\`(工作流: ${mine.state.workflow_name},步骤: ${mine.state.current_step})。\n\n使用 /ralphflow-continue 继续,或先用 /ralphflow-cancel 取消。`);
|
|
314
|
+
}
|
|
315
|
+
const workflowProblems = [];
|
|
316
|
+
const workflowDef = engine.loadWorkflow(workflow, workflowProblems);
|
|
317
|
+
if (!workflowDef) {
|
|
318
|
+
if (workflowProblems.length > 0) {
|
|
319
|
+
return text(`工作流 "${workflow}" 定义无效,无法启动:\n${workflowProblems.map((p) => `- ${p}`).join("\n")}\n\n请修复工作流 YAML 后重试。`);
|
|
320
|
+
}
|
|
321
|
+
const available = engine.listWorkflows();
|
|
322
|
+
return text(available.length > 0
|
|
323
|
+
? `工作流 "${workflow}" 未找到。可用工作流:\n${available.map((w) => `- ${w.name}: ${w.desc}`).join("\n")}`
|
|
324
|
+
: "没有找到工作流。请在 .ralph-flow/workflows/ 目录创建工作流定义文件。");
|
|
325
|
+
}
|
|
326
|
+
const firstStep = workflowDef.steps[0];
|
|
327
|
+
if (!firstStep)
|
|
328
|
+
return text("工作流没有步骤。");
|
|
329
|
+
// Validate extra_dirs before creating anything.
|
|
330
|
+
const home = os.homedir() || "";
|
|
331
|
+
const resolvedExtraDirs = [];
|
|
332
|
+
for (const d of extra_dirs) {
|
|
333
|
+
let p = String(d).trim();
|
|
334
|
+
if (!p)
|
|
335
|
+
continue;
|
|
336
|
+
if (p === "~" || p.startsWith("~/"))
|
|
337
|
+
p = path.join(home, p.slice(1));
|
|
338
|
+
if (!path.isAbsolute(p))
|
|
339
|
+
p = path.resolve(engine.projectDir, p);
|
|
340
|
+
let st = null;
|
|
341
|
+
try {
|
|
342
|
+
st = fs.statSync(p);
|
|
343
|
+
}
|
|
344
|
+
catch { }
|
|
345
|
+
if (!st || !st.isDirectory()) {
|
|
346
|
+
return text(`extra_dirs 校验失败:\`${d}\`(解析为 \`${p}\`)不存在或不是目录。请修正后重新启动。`);
|
|
347
|
+
}
|
|
348
|
+
resolvedExtraDirs.push(p);
|
|
349
|
+
}
|
|
350
|
+
// Create a fresh instance owned by this session.
|
|
351
|
+
const instId = engine.generateInstanceId(workflow);
|
|
352
|
+
fs.mkdirSync(engine.getInstanceDir(instId), { recursive: true });
|
|
353
|
+
engine.writeArtifactsDirName(instId, task);
|
|
354
|
+
engine.writeExtraDirs(instId, resolvedExtraDirs);
|
|
355
|
+
const othersNote = instances.length > 0
|
|
356
|
+
? `\n\n> ℹ️ 本目录下另有 ${instances.length} 个工作流实例,使用 /ralphflow-status 查看。`
|
|
357
|
+
: "";
|
|
358
|
+
const extraDirsNote = resolvedExtraDirs.length > 0
|
|
359
|
+
? `\n\n验证器额外可读目录:${resolvedExtraDirs.map((d) => `\`${d}\``).join("、")}`
|
|
360
|
+
: "";
|
|
361
|
+
const baseState = { active: true, workflow_name: workflow, current_step: firstStep.id, current_phase: "do", fail_count: 0, user_task: task, paused: false, session_id: sessionId };
|
|
362
|
+
const overview = stepsOverview(workflowDef.steps);
|
|
363
|
+
if (isSubWorkflowStep(firstStep)) {
|
|
364
|
+
engine.recordStepStart(instId, firstStep.id, "do");
|
|
365
|
+
engine.logEvent(instId, "info", "step_start", { step: firstStep.id, phase: "do" });
|
|
366
|
+
engine.writeState(baseState, instId);
|
|
367
|
+
engine.pushState(baseState, instId);
|
|
368
|
+
const subResult = engine.resolveSubWorkflowEntry(instId, firstStep.workflow, task, firstStep);
|
|
369
|
+
if (subResult.error) {
|
|
370
|
+
try {
|
|
371
|
+
fs.rmSync(engine.getInstanceDir(instId), { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
catch { }
|
|
374
|
+
return text(subResult.text);
|
|
375
|
+
}
|
|
376
|
+
engine.logEvent(instId, "info", "workflow_start", { workflow, instance: instId });
|
|
377
|
+
runner.ensureRunning(instId);
|
|
378
|
+
const subStartResult = await attachRunView(ctx, engine, runner, sessionId, instId);
|
|
379
|
+
return attachResult(`工作流 "${workflow}" 已启动(实例 \`${instId}\`)。\n\n任务:${task}\n\n## 步骤概览\n${overview}\n\n启动子工作流:**${firstStep.id}** → ${firstStep.workflow}${extraDirsNote}${othersNote}`, subStartResult, ctx);
|
|
380
|
+
}
|
|
381
|
+
engine.writeState(baseState, instId);
|
|
382
|
+
engine.logEvent(instId, "info", "workflow_start", { workflow, instance: instId });
|
|
383
|
+
engine.recordStepStart(instId, firstStep.id, "do");
|
|
384
|
+
engine.logEvent(instId, "info", "step_start", { step: firstStep.id, phase: "do" });
|
|
385
|
+
if (workflowDef.manual_step && workflowDef.manual_step.includes(firstStep.id)) {
|
|
386
|
+
engine.writeManualStepMarker(instId);
|
|
387
|
+
}
|
|
388
|
+
// Cache the DO prompt so the runner's first turn uses exactly this text.
|
|
389
|
+
engine.buildDoPrompt(instId, firstStep, task);
|
|
390
|
+
runner.ensureRunning(instId);
|
|
391
|
+
const startResult = await attachRunView(ctx, engine, runner, sessionId, instId);
|
|
392
|
+
return attachResult(`工作流 "${workflow}" 已启动(实例 \`${instId}\`)。\n\n任务:${task}\n\n## 步骤概览\n${overview}\n\n开始:**${firstStep.id}** - ${firstStep.desc}${extraDirsNote}${othersNote}`, startResult, ctx);
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
// ─── ralphflow_continue ─────────────────────────────────────────────────────
|
|
396
|
+
//
|
|
397
|
+
// Approve a manual review / resume a paused workflow / attach to an
|
|
398
|
+
// interrupted instance. The six branches are ported one-for-one; what changed
|
|
399
|
+
// is the tail: instead of promising the idle handler will pick it up, each
|
|
400
|
+
// branch hands the instance to the runner.
|
|
401
|
+
const ralphflow_continue = defineTool({
|
|
402
|
+
name: "ralphflow_continue",
|
|
403
|
+
label: "Ralph Flow: Continue",
|
|
404
|
+
description: "Approve a manual review / resume a paused workflow / attach to an interrupted instance. Verification runs automatically — you do not need to trigger it. (Optional instance id, unique prefix allowed.)",
|
|
405
|
+
parameters: Type.Object({
|
|
406
|
+
instance: Type.Optional(Type.String({ description: "Instance id (unique prefix allowed). Only needed to attach to a specific instance from a new session." })),
|
|
407
|
+
}),
|
|
408
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
409
|
+
const sessionId = getSessionId();
|
|
410
|
+
const resolution = engine.resolveInstance(params.instance, sessionId);
|
|
411
|
+
if (!resolution.ok)
|
|
412
|
+
return text(resolution.text);
|
|
413
|
+
const instId = resolution.id;
|
|
414
|
+
const attached = resolution.attached;
|
|
415
|
+
const foreign = foreignDriverNote(instId);
|
|
416
|
+
if (foreign)
|
|
417
|
+
return text(foreign);
|
|
418
|
+
// One session drives at most one instance.
|
|
419
|
+
if (attached) {
|
|
420
|
+
const other = engine.listInstances().find((i) => i.id !== instId && i.owner === sessionId);
|
|
421
|
+
if (other) {
|
|
422
|
+
return text(`当前会话已有活跃工作流实例 \`${other.id}\`(工作流: ${other.state.workflow_name},步骤: ${other.state.current_step})。一个会话同时只能驱动一个实例——请先完成或取消它,或在另一个会话中接管 \`${instId}\`。`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const response = await locked(instId, () => resolveContinueAction(engine, instId, sessionId, attached));
|
|
426
|
+
// Whatever branch ran, the instance is now in a state the loop can drive.
|
|
427
|
+
if (!engine.instanceExists(instId))
|
|
428
|
+
return text(response);
|
|
429
|
+
runner.ensureRunning(instId);
|
|
430
|
+
const continueResult = await attachRunView(ctx, engine, runner, sessionId, instId);
|
|
431
|
+
return attachResult(response, continueResult, ctx);
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
// ─── ralphflow_watch ────────────────────────────────────────────────────────
|
|
435
|
+
//
|
|
436
|
+
// Re-attach the run view to an instance that's already running — the
|
|
437
|
+
// detach-and-keep-running counterpart to ralphflow_start/continue. Mutates
|
|
438
|
+
// nothing by itself: no approval, no resume, no retry. If the instance
|
|
439
|
+
// isn't actually being driven yet (e.g. this is a fresh session picking up
|
|
440
|
+
// something adopted at boot), it just starts driving it, same as continue.
|
|
441
|
+
const ralphflow_watch = defineTool({
|
|
442
|
+
name: "ralphflow_watch",
|
|
443
|
+
label: "Ralph Flow: Watch",
|
|
444
|
+
description: "Attach the live run view to a workflow instance that's already running. Does not approve, resume, or cancel anything by itself. ONLY call this when the user explicitly asks to see the workflow again (\"show me\", \"let's look\", \"attach\"). Detaching (Esc) means the user chose to stop watching — do NOT call this on your own initiative just because a workflow happens to be running in the background; you'll be told in this same chat when it needs you (gate/pause) or finishes, so there's nothing to check on unprompted.",
|
|
445
|
+
parameters: Type.Object({
|
|
446
|
+
instance: Type.Optional(Type.String({ description: "Instance id (unique prefix allowed). Only needed if there's more than one active instance." })),
|
|
447
|
+
}),
|
|
448
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
449
|
+
const sessionId = getSessionId();
|
|
450
|
+
const resolution = engine.resolveInstance(params.instance, sessionId);
|
|
451
|
+
if (!resolution.ok)
|
|
452
|
+
return text(resolution.text);
|
|
453
|
+
const instId = resolution.id;
|
|
454
|
+
const foreign = foreignDriverNote(instId);
|
|
455
|
+
if (foreign)
|
|
456
|
+
return text(foreign);
|
|
457
|
+
await locked(instId, () => { engine.bindInstance(instId, sessionId); return ""; });
|
|
458
|
+
if (!engine.instanceExists(instId))
|
|
459
|
+
return text(`实例 \`${instId}\` 已经不存在。`);
|
|
460
|
+
runner.ensureRunning(instId);
|
|
461
|
+
const watchResult = await attachRunView(ctx, engine, runner, sessionId, instId);
|
|
462
|
+
return attachResult(`已接管实例 \`${instId}\`。`, watchResult, ctx);
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
// ─── ralphflow_cancel ───────────────────────────────────────────────────────
|
|
466
|
+
const ralphflow_cancel = defineTool({
|
|
467
|
+
name: "ralphflow_cancel",
|
|
468
|
+
label: "Ralph Flow: Cancel",
|
|
469
|
+
description: "Cancel a workflow instance and clean up its state (optional instance id, unique prefix allowed).",
|
|
470
|
+
parameters: Type.Object({
|
|
471
|
+
instance: Type.Optional(Type.String({ description: "Instance id (unique prefix allowed). Only needed to cancel a specific instance not bound to this session." })),
|
|
472
|
+
}),
|
|
473
|
+
execute: async (_id, params) => {
|
|
474
|
+
const sessionId = getSessionId();
|
|
475
|
+
const resolution = engine.resolveInstance(params.instance, sessionId);
|
|
476
|
+
if (!resolution.ok)
|
|
477
|
+
return text(resolution.text);
|
|
478
|
+
const instId = resolution.id;
|
|
479
|
+
const state = engine.readState(instId);
|
|
480
|
+
const workflowName = state ? state.workflow_name : instId;
|
|
481
|
+
const ownedElsewhere = state?.session_id && state.session_id !== sessionId;
|
|
482
|
+
const foreignPid = engine.foreignRunnerPid(instId);
|
|
483
|
+
const response = await locked(instId, () => {
|
|
484
|
+
engine.logEvent(instId, "info", "workflow_cancelled", { workflow: workflowName, instance: instId });
|
|
485
|
+
// destroyInstance aborts the sessions this process owns; a runner in
|
|
486
|
+
// another process stops on its next state read.
|
|
487
|
+
const reportPath = engine.destroyInstance(instId, "cancelled");
|
|
488
|
+
let out = `工作流 "${workflowName}"(实例 \`${instId}\`)已取消。`;
|
|
489
|
+
if (reportPath)
|
|
490
|
+
out += `\n执行报告:${path.relative(engine.projectDir, reportPath)}`;
|
|
491
|
+
if (foreignPid)
|
|
492
|
+
out += `\n\n> ⚠️ 该实例正被 pid ${foreignPid} 的 ralph 进程驱动。它会在下一次读取状态时停止;已在途的模型调用可能还会跑完当前一轮。`;
|
|
493
|
+
else if (ownedElsewhere)
|
|
494
|
+
out += `\n\n> ⚠️ 该实例的属主是另一个会话。它的下一次 ralphflow_continue 调用会得到"工作流已取消"。`;
|
|
495
|
+
return out;
|
|
496
|
+
});
|
|
497
|
+
return text(response);
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
// ─── ralphflow_status ───────────────────────────────────────────────────────
|
|
501
|
+
//
|
|
502
|
+
// Every return here uses terminateText, not text: status is a "report and
|
|
503
|
+
// stop" operation, nothing about it needs a follow-up tool call. Without
|
|
504
|
+
// this, a model could read the status text and decide on its own, in the
|
|
505
|
+
// same turn, to also call ralphflow_watch and take over the screen — the
|
|
506
|
+
// same unwanted chaining `terminate` already prevents on the attach tools,
|
|
507
|
+
// just reached via a different tool combination (status → watch instead of
|
|
508
|
+
// watch → watch). Reported directly by a real user, not a hypothetical.
|
|
509
|
+
const ralphflow_status = defineTool({
|
|
510
|
+
name: "ralphflow_status",
|
|
511
|
+
label: "Ralph Flow: Status",
|
|
512
|
+
description: "Show workflow status: the current session's instance, a specific instance, or an overview of all instances. Only call this when the user asks about status — a workflow running in the background does not need to be checked on; you'll be told in this same chat when it needs you or finishes.",
|
|
513
|
+
parameters: Type.Object({
|
|
514
|
+
instance: Type.Optional(Type.String({ description: "Instance id (unique prefix allowed) to inspect a specific instance." })),
|
|
515
|
+
}),
|
|
516
|
+
execute: async (_id, params) => {
|
|
517
|
+
const sessionId = getSessionId();
|
|
518
|
+
const instances = engine.listInstances();
|
|
519
|
+
// Pick the instance to detail: explicit > owned by this session.
|
|
520
|
+
let target = null;
|
|
521
|
+
if (params.instance) {
|
|
522
|
+
const wanted = String(params.instance).trim();
|
|
523
|
+
const matches = instances.filter((i) => i.id === wanted);
|
|
524
|
+
const prefixMatches = matches.length > 0 ? matches : instances.filter((i) => i.id.startsWith(wanted));
|
|
525
|
+
if (prefixMatches.length === 1)
|
|
526
|
+
target = prefixMatches[0];
|
|
527
|
+
else if (prefixMatches.length === 0) {
|
|
528
|
+
return terminateText(instances.length === 0
|
|
529
|
+
? `没有找到实例 "${wanted}"。当前没有活跃的工作流实例。`
|
|
530
|
+
: `没有找到匹配 "${wanted}" 的实例。\n\n${engine.formatInstanceList(instances)}`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
return terminateText(`前缀 "${wanted}" 匹配到多个实例:\n\n${engine.formatInstanceList(prefixMatches)}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const mine = instances.filter((i) => i.owner === sessionId);
|
|
538
|
+
if (mine.length === 1)
|
|
539
|
+
target = mine[0];
|
|
540
|
+
}
|
|
541
|
+
if (!target) {
|
|
542
|
+
if (instances.length === 0) {
|
|
543
|
+
return terminateText("没有活跃的工作流实例。使用 ralphflow_start 启动一个。");
|
|
544
|
+
}
|
|
545
|
+
return terminateText(engine.formatInstanceList(instances, "查看某个实例详情:`ralphflow_status` 传入 `instance` 参数;接管某个实例:`ralphflow_continue` 传入 `instance` 参数(支持唯一前缀)。"));
|
|
546
|
+
}
|
|
547
|
+
const state = target.state;
|
|
548
|
+
const workflow = engine.loadWorkflow(state.workflow_name);
|
|
549
|
+
const currentStep = workflow ? engine.getStep(workflow, state.current_step) : null;
|
|
550
|
+
const foreignPid = engine.foreignRunnerPid(target.id);
|
|
551
|
+
let status = `## 工作流状态
|
|
552
|
+
|
|
553
|
+
- **实例**: \`${target.id}\`
|
|
554
|
+
- **工作流**: ${state.workflow_name}
|
|
555
|
+
- **状态**: ${engine.instanceStatusLabel(target)}
|
|
556
|
+
- **当前步骤**: ${state.current_step}
|
|
557
|
+
- **当前阶段**: ${state.current_phase}
|
|
558
|
+
- **失败次数**: ${state.fail_count}
|
|
559
|
+
- **属主会话**: ${target.owner ? (target.owner === sessionId ? "🟢 本会话" : `\`${target.owner.slice(0, 8)}\``) : "无"}
|
|
560
|
+
- **最后活动**: ${engine.formatLastActivity(target.lastActivity)}`;
|
|
561
|
+
if (foreignPid)
|
|
562
|
+
status += `\n- **驱动进程**: pid ${foreignPid}(另一个 ralph 进程)`;
|
|
563
|
+
if (state.last_failure_reason)
|
|
564
|
+
status += `\n- **上次失败原因**: ${state.last_failure_reason}`;
|
|
565
|
+
if (target.manualGate) {
|
|
566
|
+
status += `\n\n> 📋 该实例正在等待手动审查。审查完成后调用 \`ralphflow_continue\` 进入验证阶段。`;
|
|
567
|
+
}
|
|
568
|
+
else if (target.owner && target.owner !== sessionId) {
|
|
569
|
+
status += `\n\n> ℹ️ 该实例的属主是另一个会话。调用 \`ralphflow_continue\`(必要时带实例 ID)可在当前会话接管并继续。`;
|
|
570
|
+
}
|
|
571
|
+
if (currentStep) {
|
|
572
|
+
if (isSubWorkflowStep(currentStep)) {
|
|
573
|
+
status += `
|
|
574
|
+
|
|
575
|
+
## 当前步骤详情
|
|
576
|
+
|
|
577
|
+
- **描述**: ${currentStep.desc}
|
|
578
|
+
- **类型**: 子工作流
|
|
579
|
+
- **子工作流**: ${currentStep.workflow}
|
|
580
|
+
- **输入**: ${currentStep.inputs ? JSON.stringify(currentStep.inputs) : "无"}
|
|
581
|
+
- **最大失败次数**: ${currentStep.max_fail_count}`;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
status += `
|
|
585
|
+
|
|
586
|
+
## 当前步骤详情
|
|
587
|
+
|
|
588
|
+
- **描述**: ${currentStep.desc}
|
|
589
|
+
- **任务**: ${currentStep.do}
|
|
590
|
+
- **输入**: ${currentStep.input || "无"}
|
|
591
|
+
- **输出**: ${currentStep.output || "无"}
|
|
592
|
+
- **检查**: ${currentStep.check}
|
|
593
|
+
- **最大失败次数**: ${currentStep.max_fail_count}`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (instances.length > 1) {
|
|
597
|
+
status += `\n\n> ℹ️ 本目录共有 ${instances.length} 个活跃实例。不带参数的 ralphflow_status 只显示当前会话的实例;传入 instance 参数查看其他实例。`;
|
|
598
|
+
}
|
|
599
|
+
return terminateText(status);
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
// ─── ralphflow_list ─────────────────────────────────────────────────────────
|
|
603
|
+
const ralphflow_list = defineTool({
|
|
604
|
+
name: "ralphflow_list",
|
|
605
|
+
label: "Ralph Flow: List",
|
|
606
|
+
description: "List all available workflows and active workflow instances.",
|
|
607
|
+
parameters: Type.Object({}),
|
|
608
|
+
execute: async () => {
|
|
609
|
+
const workflows = engine.listWorkflows();
|
|
610
|
+
let out = workflows.length > 0
|
|
611
|
+
? `## 可用工作流\n\n${workflows.map((w) => `- **${w.name}**: ${w.desc}`).join("\n")}`
|
|
612
|
+
: "没有找到工作流。请在 .ralph-flow/workflows/ 目录创建工作流定义文件。";
|
|
613
|
+
const instances = engine.listInstances();
|
|
614
|
+
if (instances.length > 0) {
|
|
615
|
+
out += `\n\n---\n\n${engine.formatInstanceList(instances)}`;
|
|
616
|
+
}
|
|
617
|
+
return text(out);
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
// ─── ralphflow_doctor ───────────────────────────────────────────────────────
|
|
621
|
+
const ralphflow_doctor = defineTool({
|
|
622
|
+
name: "ralphflow_doctor",
|
|
623
|
+
label: "Ralph Flow: Doctor",
|
|
624
|
+
description: "Diagnose all workflow definitions and skills (project + global + built-in): validation errors with full reason lists, silently-skipped steps, unreachable steps, unresolvable template tokens, broken sub-workflow references and cycles, shadowing, ignored non-workflow YAML files, and corrupt instance state. Read-only.",
|
|
625
|
+
parameters: Type.Object({}),
|
|
626
|
+
execute: async () => {
|
|
627
|
+
const index = loadSkillIndex(engine.getRalphFlowDir(), engine.getGlobalConfigHome());
|
|
628
|
+
return text(`${engine.buildDoctorReport()}\n\n## Skill\n\n${formatSkillReport(index)}`);
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
return [ralphflow_start, ralphflow_continue, ralphflow_watch, ralphflow_cancel, ralphflow_status, ralphflow_list, ralphflow_doctor];
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=tools.js.map
|