@guanlin4924/devflow-pi 0.1.98 → 0.1.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +560 -262
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +6 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_traceability.py +31 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +6 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +11 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +14 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +12 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +8 -0
- package/plugins/devflow-intel/extensions/devflow-intel.ts +25 -3
- package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +55 -28
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +41 -14
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +2 -2
- package/src/constants.ts +1 -1
- package/src/mcp/core.ts +23 -24
- package/src/mcp/server.ts +11 -12
- package/src/project-context.ts +9 -1
- package/src/project-initializer.ts +4 -4
- package/src/session-rollover.ts +10 -0
- package/src/workflow-diagnosis.ts +407 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
3
|
import { appendFile, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
@@ -16,7 +16,21 @@ import { loadProjectContext } from "../../../src/project-context.ts";
|
|
|
16
16
|
import { runProjectInitialization } from "../../../src/project-initializer.ts";
|
|
17
17
|
import { projectControlDatabasePath } from "../../../src/paths.ts";
|
|
18
18
|
import { generateProjectDocuments } from "../../../src/project-documents.ts";
|
|
19
|
+
import { preparedRolloverRevision } from "../../../src/session-rollover.ts";
|
|
19
20
|
import { describeStartupIssue, renderStartupBriefing } from "../../../src/startup-briefing.ts";
|
|
21
|
+
import { PRODUCT_VERSION } from "../../../src/constants.ts";
|
|
22
|
+
import {
|
|
23
|
+
FIX_LABELS,
|
|
24
|
+
buildHelpBundle,
|
|
25
|
+
classifyNextStepPhrase,
|
|
26
|
+
describeControlFailure,
|
|
27
|
+
diagnoseWorkflow,
|
|
28
|
+
renderDiagnosis,
|
|
29
|
+
summarizeToolFailure,
|
|
30
|
+
type FixAction,
|
|
31
|
+
type ToolFailureRecord,
|
|
32
|
+
type WorkflowDiagnosis,
|
|
33
|
+
} from "../../../src/workflow-diagnosis.ts";
|
|
20
34
|
import { exportInitializationSnapshot, importLegacyInitialization, switchInitializationAuthority } from "../../../src/project-migration.ts";
|
|
21
35
|
import { migrateLegacyProjectLayout } from "../../../src/project-layout.ts";
|
|
22
36
|
import {
|
|
@@ -37,6 +51,15 @@ import {
|
|
|
37
51
|
|
|
38
52
|
export type DevflowIntent = "idle" | "explain-state" | "project-init" | "requirement-draft" | "workflow-execute";
|
|
39
53
|
|
|
54
|
+
/** Used only after the runtime receives an explicit user checkpoint approval. */
|
|
55
|
+
export function wireframeApprovalArgs(feature: string, revision: number): string[] {
|
|
56
|
+
return [
|
|
57
|
+
"--action", "approve", "--feature", feature,
|
|
58
|
+
"--expected-revision", String(revision), "--approved-by", "user",
|
|
59
|
+
"--all-screens", "--runtime-confirmed", "--write", "--json",
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
export type ControlPhase = "idle" | "planning" | "awaiting-approval" | "waiting-user" | "executing" | "blocked";
|
|
41
64
|
|
|
42
65
|
export type SessionControlState = {
|
|
@@ -581,6 +604,10 @@ export function isVirtualStateTransitionResource(toolName: string, input: unknow
|
|
|
581
604
|
return [record.path, record.uri, record.resource].some(value => value === "xd://devflow_transition");
|
|
582
605
|
}
|
|
583
606
|
|
|
607
|
+
export const OUTSIDE_STEP_WRITE_REASON =
|
|
608
|
+
"当前不在受控的工作流步骤中:在这里写入 .devflow/planning/requirements 或 features 下的需求产物不会被工作流采纳,已拒绝。" +
|
|
609
|
+
"不要改用其他方式写入这些文件。请停止写入,把已有结论直接告诉用户,并请用户说「下一步」(还没有需求时先创建需求),由 Devflow 启动对应步骤后再写。";
|
|
610
|
+
|
|
584
611
|
export function planningToolBlockReason(toolName: string): string {
|
|
585
612
|
return `Devflow ${toolName} blocked: current intent is planning. Planning is read-only; present an Evidence Pack or plan and wait for explicit approval before writing or executing.`;
|
|
586
613
|
}
|
|
@@ -756,6 +783,9 @@ type DoctorContext = {
|
|
|
756
783
|
};
|
|
757
784
|
};
|
|
758
785
|
|
|
786
|
+
type DevflowCommandHandler = Parameters<ExtensionAPI["registerCommand"]>[1]["handler"];
|
|
787
|
+
type DevflowCommandContext = Parameters<DevflowCommandHandler>[1];
|
|
788
|
+
|
|
759
789
|
const CONTROL_PATHS = [
|
|
760
790
|
".devflow/planning/STATE.md",
|
|
761
791
|
".devflow/planning/.next-intent",
|
|
@@ -1131,8 +1161,31 @@ async function runProcess(
|
|
|
1131
1161
|
};
|
|
1132
1162
|
}
|
|
1133
1163
|
|
|
1164
|
+
/** Raw failures kept in memory for the user's help bundle; never sent to the model. */
|
|
1165
|
+
const recentToolFailures: ToolFailureRecord[] = [];
|
|
1166
|
+
|
|
1167
|
+
function recordToolFailure(tool: string, result: ProcessResult, summary: string): void {
|
|
1168
|
+
recentToolFailures.push({
|
|
1169
|
+
at: new Date().toISOString(),
|
|
1170
|
+
tool,
|
|
1171
|
+
exitCode: result.exitCode,
|
|
1172
|
+
summary,
|
|
1173
|
+
raw: (result.stderr || result.stdout).slice(0, 4000),
|
|
1174
|
+
});
|
|
1175
|
+
if (recentToolFailures.length > 20) recentToolFailures.splice(0, recentToolFailures.length - 20);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1134
1178
|
function renderResult(result: ProcessResult, details: Record<string, unknown> = {}) {
|
|
1135
|
-
|
|
1179
|
+
// JSON output passes through unchanged; plain-text failures and empty output
|
|
1180
|
+
// become a Chinese summary instead of a raw stack trace.
|
|
1181
|
+
const summary = summarizeToolFailure(result);
|
|
1182
|
+
if (summary && !result.ok) recordToolFailure(String(details.action ?? "devflow"), result, summary);
|
|
1183
|
+
const raw = (result.stderr || result.stdout).trim();
|
|
1184
|
+
// Stack traces collapse to the summary; other plain-text failures may list
|
|
1185
|
+
// several problems, so the model still sees them in full.
|
|
1186
|
+
const keepRaw = summary !== null && !result.ok && raw !== "" && !summary.includes(raw)
|
|
1187
|
+
&& !/:\d+:in [`']|Traceback \(most recent call last\)/u.test(raw);
|
|
1188
|
+
const text = summary === null ? (result.stdout || result.stderr) : keepRaw ? `${summary}\n\n原始信息:\n${raw.slice(0, 2000)}` : summary;
|
|
1136
1189
|
return {
|
|
1137
1190
|
content: [{ type: "text" as const, text }],
|
|
1138
1191
|
details: { ...details, exitCode: result.exitCode, stderr: result.stderr || undefined },
|
|
@@ -1261,7 +1314,7 @@ function selectedWorkflowStep(snapshot: Record<string, unknown>): Record<string,
|
|
|
1261
1314
|
* completed step. This predicate lets the runtime re-arm approval handling
|
|
1262
1315
|
* from the Core-owned snapshot without granting approval by itself.
|
|
1263
1316
|
*/
|
|
1264
|
-
function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown
|
|
1317
|
+
function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>, options: { strictRegistry?: boolean } = {}): boolean {
|
|
1265
1318
|
const status = snapshot.status;
|
|
1266
1319
|
if (status !== "waiting_checkpoint" && status !== "not_started" && status !== "in_progress") return false;
|
|
1267
1320
|
const step = snapshot.current_step;
|
|
@@ -1284,9 +1337,15 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolea
|
|
|
1284
1337
|
// Core has already validated and registered the artifact. Treat an
|
|
1285
1338
|
// explicit false as invalid, but use exists + artifact_status as the
|
|
1286
1339
|
// durable fallback when the field is absent.
|
|
1340
|
+
// sync_artifacts.rb keeps every output of an unfinished non-checkpoint
|
|
1341
|
+
// step at `planned`, so `planned` must not hide an output the step has
|
|
1342
|
+
// already written. advance_state.rb re-validates outputs on transition.
|
|
1343
|
+
// Resume disposition stays strict so an existing file never lets a step
|
|
1344
|
+
// start without contract activation.
|
|
1345
|
+
const rejected = options.strictRegistry ? ["failed", "blocked", "planned"] : ["failed", "blocked"];
|
|
1287
1346
|
return record.exists === true
|
|
1288
1347
|
&& record.content_valid !== false
|
|
1289
|
-
&& !
|
|
1348
|
+
&& !rejected.includes(String(record.artifact_status ?? ""));
|
|
1290
1349
|
});
|
|
1291
1350
|
}
|
|
1292
1351
|
|
|
@@ -1314,13 +1373,13 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
|
|
|
1314
1373
|
if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
|
|
1315
1374
|
|
|
1316
1375
|
if (action === "await_user_confirmation") {
|
|
1317
|
-
return isReviewableWorkflowSnapshot(snapshot) ? "review" : "blocked";
|
|
1376
|
+
return isReviewableWorkflowSnapshot(snapshot, { strictRegistry: true }) ? "review" : "blocked";
|
|
1318
1377
|
}
|
|
1319
1378
|
// Parallel windows must be reconstructed by the Runtime even if a stale
|
|
1320
1379
|
// join artifact happens to exist; branch lifecycle is the join authority.
|
|
1321
1380
|
if (action === "run_parallel_window" || action === "route_failure_branch") return "execute";
|
|
1322
1381
|
if (action === "run_next_step") {
|
|
1323
|
-
return isReviewableWorkflowSnapshot(snapshot) ? "review" : "execute";
|
|
1382
|
+
return isReviewableWorkflowSnapshot(snapshot, { strictRegistry: true }) ? "review" : "execute";
|
|
1324
1383
|
}
|
|
1325
1384
|
return "blocked";
|
|
1326
1385
|
}
|
|
@@ -2675,6 +2734,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2675
2734
|
}
|
|
2676
2735
|
|
|
2677
2736
|
let activeWorkflowTurn = false;
|
|
2737
|
+
// Set once the /devflow command is defined; lets the input handler and the
|
|
2738
|
+
// next-step entry reuse existing subcommands (start, abandon).
|
|
2739
|
+
let devflowCommandRef: ((args: string, ctx: DevflowCommandContext) => Promise<void>) | undefined;
|
|
2678
2740
|
let parallelRunActive = false;
|
|
2679
2741
|
let parallelContinuationQueued = false;
|
|
2680
2742
|
let parallelDispatchSucceededThisTurn = false;
|
|
@@ -2880,6 +2942,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2880
2942
|
ctx.ui.notify("已保留当前步骤,未推进工作流。", "info");
|
|
2881
2943
|
return true;
|
|
2882
2944
|
}
|
|
2945
|
+
if (checkpoint && stepId === "wireframe-review") {
|
|
2946
|
+
const feature = inspected.snapshot.current_feature;
|
|
2947
|
+
if (typeof feature !== "string") return false;
|
|
2948
|
+
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
|
|
2949
|
+
wireframeApprovalArgs(feature, revision));
|
|
2950
|
+
if (!controlPlaneSucceeded(approval)) {
|
|
2951
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
|
|
2952
|
+
ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
|
|
2953
|
+
return true;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2883
2956
|
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
2884
2957
|
"--write", "--json", "--expected-revision", String(revision),
|
|
2885
2958
|
...(checkpoint ? ["--confirm-checkpoint", "--runtime-confirmed"] : ["--complete-step", stepId]),
|
|
@@ -2887,7 +2960,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2887
2960
|
]);
|
|
2888
2961
|
if (!result.ok || !controlPlaneSucceeded(result)) {
|
|
2889
2962
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
2890
|
-
ctx.ui.notify(`未推进:${result
|
|
2963
|
+
ctx.ui.notify(`未推进:${describeControlFailure(result)}\n可以说「下一步」重新诊断,或选择重新执行这一步来修复产物。`, "error");
|
|
2891
2964
|
return true;
|
|
2892
2965
|
}
|
|
2893
2966
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
@@ -2909,6 +2982,183 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2909
2982
|
return true;
|
|
2910
2983
|
}
|
|
2911
2984
|
|
|
2985
|
+
async function diagnoseCurrentWorkflow(cwd: string): Promise<{
|
|
2986
|
+
snapshot: Record<string, unknown> | null;
|
|
2987
|
+
inspectError?: string;
|
|
2988
|
+
rolloverState: Record<string, unknown> | null;
|
|
2989
|
+
diagnosis: WorkflowDiagnosis;
|
|
2990
|
+
}> {
|
|
2991
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(cwd);
|
|
2992
|
+
// A missing STATE.md is reported as structured JSON, not always with exit 0.
|
|
2993
|
+
const snapshot = inspected.snapshot ?? parseJsonOutput(inspected.result);
|
|
2994
|
+
const inspectError = snapshot ? undefined : (inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON");
|
|
2995
|
+
const rolloverState = snapshot?.current_feature
|
|
2996
|
+
? parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]))
|
|
2997
|
+
: null;
|
|
2998
|
+
const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
2999
|
+
const diagnosis = diagnoseWorkflow({
|
|
3000
|
+
inspect: { ok: Boolean(snapshot), error: inspectError },
|
|
3001
|
+
snapshot,
|
|
3002
|
+
disposition: snapshot ? workflowResumeDisposition(snapshot) : "idle",
|
|
3003
|
+
outputsReady: snapshot ? isReviewableWorkflowSnapshot(snapshot) : false,
|
|
3004
|
+
rolloverPrepared: Boolean(rolloverState && typeof rolloverState.revision === "number"
|
|
3005
|
+
&& rollover?.status === "prepared" && typeof rollover.id === "string"),
|
|
3006
|
+
});
|
|
3007
|
+
return { snapshot, inspectError, rolloverState, diagnosis };
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
async function writeHelpBundle(
|
|
3011
|
+
ctx: DevflowCommandContext,
|
|
3012
|
+
current: Awaited<ReturnType<typeof diagnoseCurrentWorkflow>>,
|
|
3013
|
+
): Promise<string> {
|
|
3014
|
+
const report = await runDoctor(ctx).catch(() => undefined);
|
|
3015
|
+
const createdAt = new Date().toISOString();
|
|
3016
|
+
const bundle = buildHelpBundle({
|
|
3017
|
+
version: PRODUCT_VERSION,
|
|
3018
|
+
createdAt,
|
|
3019
|
+
diagnosis: current.diagnosis,
|
|
3020
|
+
snapshot: current.snapshot,
|
|
3021
|
+
inspectError: current.inspectError,
|
|
3022
|
+
doctor: report?.checks,
|
|
3023
|
+
rollover: current.rolloverState,
|
|
3024
|
+
failures: recentToolFailures,
|
|
3025
|
+
root: ctx.cwd,
|
|
3026
|
+
home: homedir(),
|
|
3027
|
+
});
|
|
3028
|
+
// .devflow/runs is the git-ignored runtime output root.
|
|
3029
|
+
const relative = `.devflow/runs/diagnostics/help-${createdAt.replace(/[:.]/g, "-")}.json`;
|
|
3030
|
+
await mkdir(path.dirname(path.join(ctx.cwd, relative)), { recursive: true });
|
|
3031
|
+
await writeFile(path.join(ctx.cwd, relative), `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
|
|
3032
|
+
return relative;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
function nextStepHint(diagnosis: WorkflowDiagnosis): string {
|
|
3036
|
+
if (diagnosis.kind === "blocked") return "运行 /devflow fix 查看处理办法。";
|
|
3037
|
+
if (diagnosis.kind === "no-feature" || diagnosis.kind === "complete") return "说「下一步」或运行 /devflow start <需求名> 创建需求。";
|
|
3038
|
+
return "说「下一步」或运行 /devflow next 继续。";
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
async function showWorkflowOverview(ctx: DevflowCommandContext): Promise<void> {
|
|
3042
|
+
const { diagnosis } = await diagnoseCurrentWorkflow(ctx.cwd);
|
|
3043
|
+
ctx.ui.notify(`${renderDiagnosis(diagnosis)}\n\n${nextStepHint(diagnosis)}`, diagnosis.kind === "blocked" ? "error" : "info");
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
/** Fixes that start or advance a step; `/devflow fix` never offers them. */
|
|
3047
|
+
const ADVANCING_FIXES = new Set<FixAction>(["review-and-advance", "rerun-step", "resume-rollover", "create-feature"]);
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* Single user entry: diagnose → offer a fix from the closed catalog → let
|
|
3051
|
+
* Core execute it → re-diagnose. The model is never asked to repair control
|
|
3052
|
+
* state; every write goes through the existing runtime/Core paths.
|
|
3053
|
+
*/
|
|
3054
|
+
async function runNextStep(ctx: DevflowCommandContext, mode: "next" | "fix"): Promise<void> {
|
|
3055
|
+
if (activeWorkflowTurn || controlState.phase === "executing") {
|
|
3056
|
+
ctx.ui.notify("当前步骤正在执行,等这一轮结束后再说「下一步」。", "info");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (mode === "next") {
|
|
3060
|
+
const report = await runDoctor(ctx);
|
|
3061
|
+
if (!report.ok) {
|
|
3062
|
+
ctx.ui.notify(`运行环境检查没有通过,暂时不能推进:\n${formatDoctorReport(report).slice(0, 6000)}`, "error");
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
3067
|
+
const current = await diagnoseCurrentWorkflow(ctx.cwd);
|
|
3068
|
+
const { diagnosis, snapshot } = current;
|
|
3069
|
+
const summary = renderDiagnosis(diagnosis);
|
|
3070
|
+
if (diagnosis.kind === "execute" && snapshot) {
|
|
3071
|
+
if (mode === "fix") {
|
|
3072
|
+
ctx.ui.notify(`${summary}\n${nextStepHint(diagnosis)}`, "info");
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
await resumeInspectedWorkflow(ctx, snapshot, { doctorAlreadyPassed: true });
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
const fixes = diagnosis.diagnoses
|
|
3079
|
+
.flatMap(item => item.fixes)
|
|
3080
|
+
.filter(fix => mode === "next" || !ADVANCING_FIXES.has(fix));
|
|
3081
|
+
ctx.ui.notify(fixes.length > 0 || mode === "next" ? summary : `${summary}\n${nextStepHint(diagnosis)}`, diagnosis.kind === "blocked" ? "error" : "info");
|
|
3082
|
+
if (fixes.length === 0) return;
|
|
3083
|
+
const skip = "暂不处理";
|
|
3084
|
+
const choice = await ctx.ui.select("选择处理方式", [
|
|
3085
|
+
...fixes.map(fix => ({ label: FIX_LABELS[fix].label, description: FIX_LABELS[fix].description })),
|
|
3086
|
+
{ label: skip, description: "保持现状,不做任何修改" },
|
|
3087
|
+
]);
|
|
3088
|
+
const fix = fixes.find(item => FIX_LABELS[item].label === choice);
|
|
3089
|
+
if (!fix) {
|
|
3090
|
+
ctx.ui.notify("已保持现状,未做任何修改。", "info");
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
if (fix === "create-feature") {
|
|
3094
|
+
const name = (await ctx.ui.input("创建需求", "需求名称,例如:私海线索转回公海保护"))?.trim();
|
|
3095
|
+
if (!name) {
|
|
3096
|
+
ctx.ui.notify("未输入需求名称,未创建需求。", "info");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
await devflowCommandRef?.(`start ${name}`, ctx);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
if (fix === "review-and-advance") {
|
|
3103
|
+
setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
|
|
3104
|
+
if (!(await confirmAndAdvanceWorkflowStep(ctx))) {
|
|
3105
|
+
ctx.ui.notify("无法打开推进确认:当前步骤的产物已经变化。请重新说「下一步」。", "error");
|
|
3106
|
+
}
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
if (fix === "revise-outputs") {
|
|
3110
|
+
setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
|
|
3111
|
+
ctx.ui.notify("已回到审查状态。请直接说明需要修改的内容;改完后说「批准执行」或「下一步」推进。", "info");
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (fix === "rerun-step" && snapshot) {
|
|
3115
|
+
const stepId = diagnosis.stepId ?? "当前步骤";
|
|
3116
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
3117
|
+
void startWorkflowAfterPreflight(ctx, snapshot, "start", {
|
|
3118
|
+
doctorAlreadyPassed: true,
|
|
3119
|
+
executingLabel: `重新执行 ${stepId}`,
|
|
3120
|
+
}).catch(error => {
|
|
3121
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
3122
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
3123
|
+
});
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (fix === "resume-rollover" && snapshot) {
|
|
3127
|
+
const rollover = current.rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
3128
|
+
if (typeof current.rolloverState?.revision !== "number" || typeof rollover?.id !== "string") return;
|
|
3129
|
+
pendingRollover = { snapshot, preparedRevision: current.rolloverState.revision, rolloverId: rollover.id };
|
|
3130
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, ctx.sessionManager.getSessionId());
|
|
3131
|
+
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
3132
|
+
ctx.ui.notify("已恢复未完成的会话交接;当前回合结束后会切换到新会话并继续。", "info");
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
if (fix === "repair-registry") {
|
|
3136
|
+
const plan = snapshot?.recovery_plan as Record<string, unknown> | undefined;
|
|
3137
|
+
if (typeof plan?.expected_revision !== "number") return;
|
|
3138
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
|
|
3139
|
+
"--write", "--confirm", "--expected-revision", String(plan.expected_revision), "--json",
|
|
3140
|
+
]);
|
|
3141
|
+
if (!controlPlaneSucceeded(result)) {
|
|
3142
|
+
ctx.ui.notify(`登记表修复失败:${describeControlFailure(result)}`, "error");
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
ctx.ui.notify("登记表已修复,未推进工作流。正在重新诊断……", "info");
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
if (fix === "help-bundle") {
|
|
3149
|
+
const bundlePath = await writeHelpBundle(ctx, current);
|
|
3150
|
+
ctx.ui.notify(`已生成求助包:${bundlePath}\n只包含诊断信息(状态摘要、校验错误、最近的工具报错),不含源码。可以把这个文件发给维护者。`, "info");
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (fix === "cancel-feature") {
|
|
3154
|
+
await devflowCommandRef?.("abandon", ctx);
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
ctx.ui.notify("自动修复后仍有问题。请运行 /devflow fix 并选择「生成求助包」。", "error");
|
|
3160
|
+
}
|
|
3161
|
+
|
|
2912
3162
|
type ProjectControlCommandContext = {
|
|
2913
3163
|
cwd: string;
|
|
2914
3164
|
models: DoctorContext["models"];
|
|
@@ -3321,7 +3571,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3321
3571
|
// sole authority: only the original source session may resume a prepared
|
|
3322
3572
|
// transition, and only the already-recorded target may consume a claim.
|
|
3323
3573
|
const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
|
|
3324
|
-
|
|
3574
|
+
let rolloverState = parseJsonResult(rolloverResult);
|
|
3325
3575
|
let rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
3326
3576
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
3327
3577
|
if (
|
|
@@ -3334,13 +3584,14 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3334
3584
|
]);
|
|
3335
3585
|
if (recovered.ok) {
|
|
3336
3586
|
const refreshed = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]));
|
|
3587
|
+
rolloverState = refreshed;
|
|
3337
3588
|
rollover = refreshed?.rollover as Record<string, unknown> | undefined;
|
|
3338
3589
|
}
|
|
3339
3590
|
}
|
|
3340
3591
|
if (rolloverState && typeof rolloverState.revision === "number" && rollover?.status === "prepared" && typeof rollover.id === "string") {
|
|
3341
3592
|
const workflow = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
3342
|
-
const
|
|
3343
|
-
if (workflow.result.ok && workflow.snapshot &&
|
|
3593
|
+
const preparedRevision = preparedRolloverRevision(rolloverState);
|
|
3594
|
+
if (workflow.result.ok && workflow.snapshot && preparedRevision !== undefined && workflow.snapshot.revision === preparedRevision) {
|
|
3344
3595
|
pendingRollover = { snapshot: workflow.snapshot, preparedRevision: rolloverState.revision, rolloverId: rollover.id };
|
|
3345
3596
|
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
3346
3597
|
ctx.ui.notify("已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
@@ -3507,7 +3758,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3507
3758
|
if (event.source !== "interactive") return;
|
|
3508
3759
|
const controlAction = classifyNaturalControlAction(event.text);
|
|
3509
3760
|
const stepNote = event.text.trim();
|
|
3510
|
-
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/")) {
|
|
3761
|
+
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/") && !classifyNextStepPhrase(stepNote)) {
|
|
3511
3762
|
stepUserNotes.push(stepNote);
|
|
3512
3763
|
}
|
|
3513
3764
|
if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
|
|
@@ -3561,6 +3812,24 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3561
3812
|
}
|
|
3562
3813
|
return { handled: true };
|
|
3563
3814
|
}
|
|
3815
|
+
// Short phrases such as "下一步" open the same diagnose-and-fix entry as
|
|
3816
|
+
// /devflow next. They never approve by themselves: advancing still needs
|
|
3817
|
+
// the runtime confirmation dialog. "继续" is only taken over when a
|
|
3818
|
+
// feature is in progress so ordinary chat continuation reaches the model.
|
|
3819
|
+
const nextPhrase = classifyNextStepPhrase(event.text);
|
|
3820
|
+
if (nextPhrase && ctx.hasUI && ["idle", "waiting-user", "blocked"].includes(controlState.phase)) {
|
|
3821
|
+
let takeOver = nextPhrase === "next";
|
|
3822
|
+
if (nextPhrase === "continue") {
|
|
3823
|
+
const inspected = await inspectWorkflow(ctx.cwd);
|
|
3824
|
+
takeOver = typeof inspected.snapshot?.current_feature === "string"
|
|
3825
|
+
&& !["completed", "cancelled", "failed"].includes(String(inspected.snapshot.status));
|
|
3826
|
+
}
|
|
3827
|
+
if (takeOver) {
|
|
3828
|
+
// runNextStep only uses members shared by event and command contexts.
|
|
3829
|
+
await runNextStep(ctx as DevflowCommandContext, "next");
|
|
3830
|
+
return { handled: true };
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3564
3833
|
// The in-memory prompt state is intentionally not authoritative. A fresh
|
|
3565
3834
|
// session starts idle, so recover the approval arm from the durable Core
|
|
3566
3835
|
// snapshot before handing an explicit approval phrase to the model. This
|
|
@@ -3596,10 +3865,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3596
3865
|
return { handled: true };
|
|
3597
3866
|
}
|
|
3598
3867
|
if (checkpoint && stepId === "wireframe-review" && typeof inspected.snapshot.current_feature === "string") {
|
|
3599
|
-
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
|
|
3600
|
-
|
|
3601
|
-
"--expected-revision", String(revision), "--all-screens", "--runtime-confirmed", "--write", "--json",
|
|
3602
|
-
]);
|
|
3868
|
+
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
|
|
3869
|
+
wireframeApprovalArgs(inspected.snapshot.current_feature, revision));
|
|
3603
3870
|
if (!controlPlaneSucceeded(approval)) {
|
|
3604
3871
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
|
|
3605
3872
|
ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
|
|
@@ -3615,7 +3882,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3615
3882
|
]);
|
|
3616
3883
|
if (!result.ok || !controlPlaneSucceeded(result)) {
|
|
3617
3884
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
3618
|
-
ctx.ui.notify(`未推进:${result
|
|
3885
|
+
ctx.ui.notify(`未推进:${describeControlFailure(result)}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可以说「下一步」重新诊断,或选择重新执行这一步来修复产物。`, "error");
|
|
3619
3886
|
return { handled: true };
|
|
3620
3887
|
}
|
|
3621
3888
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
@@ -3852,6 +4119,20 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3852
4119
|
reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
|
|
3853
4120
|
};
|
|
3854
4121
|
}
|
|
4122
|
+
// Feature artifacts written while no step is running are never adopted by
|
|
4123
|
+
// the workflow (zhaoshang, 2026-10-22). Refuse early in the interactive
|
|
4124
|
+
// main session; subagents (no UI), review-feedback edits and controlled
|
|
4125
|
+
// turns keep their existing rules.
|
|
4126
|
+
if (
|
|
4127
|
+
ctx.hasUI &&
|
|
4128
|
+
controlState.phase === "idle" &&
|
|
4129
|
+
!activeWorkflowTurn &&
|
|
4130
|
+
!toolName.startsWith("devflow_") &&
|
|
4131
|
+
["write", "edit", "apply_patch", "notebook_edit"].includes(toolName) &&
|
|
4132
|
+
inputPaths(event.input, ctx.cwd).some(filePath => planningFeatureFromPath(filePath) !== null)
|
|
4133
|
+
) {
|
|
4134
|
+
return { block: true, reason: OUTSIDE_STEP_WRITE_REASON };
|
|
4135
|
+
}
|
|
3855
4136
|
if (
|
|
3856
4137
|
!toolName.startsWith("devflow_") &&
|
|
3857
4138
|
["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
|
|
@@ -4327,6 +4608,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4327
4608
|
kind: z.string().optional(),
|
|
4328
4609
|
match: z.string().optional(),
|
|
4329
4610
|
expected: z.unknown().optional(),
|
|
4611
|
+
normalize_whitespace: z.boolean().optional(),
|
|
4612
|
+
case_sensitive: z.boolean().optional(),
|
|
4330
4613
|
name: z.string().optional(),
|
|
4331
4614
|
tc_id: z.string().optional(),
|
|
4332
4615
|
}),
|
|
@@ -4385,7 +4668,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4385
4668
|
Object.assign(requestParams, { ref: params.ref, action: params.operation, value: params.value, value_from_secret: params.value_from_secret });
|
|
4386
4669
|
} else if (params.action === "assert") {
|
|
4387
4670
|
if (!params.ref || !params.kind || !["text", "visible", "value", "each_text"].includes(params.kind)) return fail("assert requires ref and a supported kind");
|
|
4388
|
-
Object.assign(requestParams, { ref: params.ref, kind: params.kind, match: params.match, expected: params.expected });
|
|
4671
|
+
Object.assign(requestParams, { ref: params.ref, kind: params.kind, match: params.match, expected: params.expected, normalize_whitespace: params.normalize_whitespace, case_sensitive: params.case_sensitive });
|
|
4389
4672
|
} else if (params.action === "draft") {
|
|
4390
4673
|
if (!params.name) return fail("draft requires name");
|
|
4391
4674
|
Object.assign(requestParams, { name: params.name, tc_id: params.tc_id });
|
|
@@ -5237,273 +5520,288 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5237
5520
|
},
|
|
5238
5521
|
});
|
|
5239
5522
|
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5523
|
+
const devflowCommand = async (args: string, ctx: DevflowCommandContext): Promise<void> => {
|
|
5524
|
+
const trimmed = args.trim();
|
|
5525
|
+
if (trimmed === "" || trimmed === "status") {
|
|
5526
|
+
await showWorkflowOverview(ctx);
|
|
5527
|
+
return;
|
|
5528
|
+
}
|
|
5529
|
+
if (trimmed === "status --json") {
|
|
5530
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]);
|
|
5531
|
+
ctx.ui.notify((result.stdout || result.stderr).slice(0, 6000), result.ok ? "info" : "error");
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
5534
|
+
if (trimmed === "next" || trimmed === "下一步") {
|
|
5535
|
+
await runNextStep(ctx, "next");
|
|
5536
|
+
return;
|
|
5537
|
+
}
|
|
5538
|
+
if (trimmed === "fix") {
|
|
5539
|
+
await runNextStep(ctx, "fix");
|
|
5540
|
+
return;
|
|
5541
|
+
}
|
|
5542
|
+
if (trimmed === "control") {
|
|
5543
|
+
await openProjectControlPanel(ctx);
|
|
5544
|
+
return;
|
|
5545
|
+
}
|
|
5546
|
+
if (trimmed === "explain-state") {
|
|
5547
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5548
|
+
ctx.ui.notify(
|
|
5549
|
+
inspected.result.ok ? explainWorkflowSnapshot(inspected.snapshot) : `状态读取失败:${inspected.result.stderr || inspected.result.stdout}`,
|
|
5550
|
+
inspected.result.ok ? "info" : "error",
|
|
5551
|
+
);
|
|
5552
|
+
setControlState(ctx, initialControlState());
|
|
5553
|
+
return;
|
|
5554
|
+
}
|
|
5555
|
+
if (trimmed === "recover") {
|
|
5556
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5557
|
+
const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
|
|
5558
|
+
const revision = plan?.expected_revision;
|
|
5559
|
+
if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
|
|
5560
|
+
const status = typeof inspected.snapshot?.status === "string" ? inspected.snapshot.status : "unknown";
|
|
5561
|
+
const step = inspected.snapshot ? selectedWorkflowStep(inspected.snapshot) : null;
|
|
5562
|
+
const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
|
|
5563
|
+
if (inspected.result.ok && status === "in_progress") {
|
|
5564
|
+
ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
|
|
5565
|
+
} else if (inspected.result.ok && status === "waiting_checkpoint") {
|
|
5566
|
+
ctx.ui.notify("控制面状态一致,无需恢复。当前正在等待 checkpoint;请审查产物后明确回复“批准执行”或提出修改意见。", "info");
|
|
5567
|
+
} else {
|
|
5568
|
+
ctx.ui.notify("当前状态没有可安全自动执行的恢复计划;请运行 /devflow explain-state,并按其中的阻塞证据处理。", "error");
|
|
5569
|
+
}
|
|
5247
5570
|
return;
|
|
5248
5571
|
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5572
|
+
const confirmed = await ctx.ui.confirm(
|
|
5573
|
+
"确认修复 Devflow 控制面",
|
|
5574
|
+
"将只根据已存在的文件同步 artifact registry;不会推进步骤、确认 checkpoint 或修改需求产物。继续吗?",
|
|
5575
|
+
);
|
|
5576
|
+
if (!confirmed) {
|
|
5577
|
+
ctx.ui.notify("恢复未确认;状态保持不变。", "info");
|
|
5251
5578
|
return;
|
|
5252
5579
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5580
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
|
|
5581
|
+
"--write",
|
|
5582
|
+
"--confirm",
|
|
5583
|
+
"--expected-revision",
|
|
5584
|
+
String(revision),
|
|
5585
|
+
"--json",
|
|
5586
|
+
]);
|
|
5587
|
+
if (!result.ok) {
|
|
5588
|
+
ctx.ui.notify(`控制面恢复失败:${result.stderr || result.stdout}`, "error");
|
|
5589
|
+
return;
|
|
5590
|
+
}
|
|
5591
|
+
ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
|
|
5592
|
+
return;
|
|
5593
|
+
}
|
|
5594
|
+
if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
|
|
5595
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5596
|
+
const snapshot = inspected.snapshot;
|
|
5597
|
+
const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
|
|
5598
|
+
const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
|
|
5599
|
+
const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
|
|
5600
|
+
if (!inspected.result.ok || revision === undefined || !feature) {
|
|
5601
|
+
ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
|
|
5602
|
+
return;
|
|
5603
|
+
}
|
|
5604
|
+
if (!status || ["completed", "cancelled", "failed"].includes(status)) {
|
|
5605
|
+
ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
|
|
5609
|
+
const confirmed = await ctx.ui.confirm(
|
|
5610
|
+
"确认弃用当前需求",
|
|
5611
|
+
`需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
|
|
5612
|
+
);
|
|
5613
|
+
if (!confirmed) {
|
|
5614
|
+
ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
|
|
5615
|
+
return;
|
|
5616
|
+
}
|
|
5617
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
5618
|
+
"--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
|
|
5619
|
+
]);
|
|
5620
|
+
if (!result.ok) {
|
|
5621
|
+
ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
|
|
5622
|
+
return;
|
|
5623
|
+
}
|
|
5624
|
+
activeContract = undefined;
|
|
5625
|
+
contractAgentCalls = 0;
|
|
5626
|
+
setControlState(ctx, initialControlState());
|
|
5627
|
+
ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5630
|
+
if (trimmed === "project-init") {
|
|
5631
|
+
const initialization = await collectProjectInitialization(ctx);
|
|
5632
|
+
if (!initialization) return;
|
|
5633
|
+
pendingProjectInitialization = initialization;
|
|
5634
|
+
if (initialization.mode === "apply") {
|
|
5635
|
+
setControlState(ctx, executingState("执行已确认的项目初始化"));
|
|
5636
|
+
activeWorkflowTurn = true;
|
|
5637
|
+
await pi.sendUserMessage(projectInitializationDirective(initialization.summary));
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
setControlState(ctx, planningState("project-init"));
|
|
5641
|
+
await pi.sendUserMessage(
|
|
5642
|
+
[
|
|
5643
|
+
"<devflow-planner-directive>",
|
|
5644
|
+
"用户已确认项目初始化的输入。你现在是 Planner,只允许只读分析和派发 project-scout / requirements-auditor / contract-auditor。",
|
|
5645
|
+
"不得创建 .devflow/planning 文件、不得调用状态迁移、不得执行实现。",
|
|
5646
|
+
"先输出:已知事实、待验证点、最少侦察任务和必须由用户决定的问题。没有证据时写 unknown。",
|
|
5647
|
+
initialization.summary,
|
|
5648
|
+
"</devflow-planner-directive>",
|
|
5649
|
+
].join("\n"),
|
|
5650
|
+
);
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
if (trimmed === "doctor") {
|
|
5654
|
+
const report = await runDoctor(ctx);
|
|
5655
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), report.ok ? "info" : "error");
|
|
5656
|
+
return;
|
|
5657
|
+
}
|
|
5658
|
+
if (trimmed === "resume") {
|
|
5659
|
+
const report = await runDoctor(ctx);
|
|
5660
|
+
if (!report.ok) {
|
|
5661
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
5662
|
+
return;
|
|
5663
|
+
}
|
|
5664
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5665
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5255
5666
|
ctx.ui.notify(
|
|
5256
|
-
|
|
5257
|
-
|
|
5667
|
+
`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`,
|
|
5668
|
+
"error",
|
|
5258
5669
|
);
|
|
5259
|
-
setControlState(ctx, initialControlState());
|
|
5260
5670
|
return;
|
|
5261
5671
|
}
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
);
|
|
5283
|
-
if (!confirmed) {
|
|
5284
|
-
ctx.ui.notify("恢复未确认;状态保持不变。", "info");
|
|
5285
|
-
return;
|
|
5286
|
-
}
|
|
5287
|
-
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
|
|
5288
|
-
"--write",
|
|
5289
|
-
"--confirm",
|
|
5290
|
-
"--expected-revision",
|
|
5291
|
-
String(revision),
|
|
5292
|
-
"--json",
|
|
5293
|
-
]);
|
|
5294
|
-
if (!result.ok) {
|
|
5295
|
-
ctx.ui.notify(`控制面恢复失败:${result.stderr || result.stdout}`, "error");
|
|
5296
|
-
return;
|
|
5297
|
-
}
|
|
5298
|
-
ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
|
|
5672
|
+
// Re-arm a durable rollover left by an interrupted runtime. An
|
|
5673
|
+
// explicit resume is the user's authorization for a fresh session to
|
|
5674
|
+
// adopt the same project's integrity-checked prepared handoff.
|
|
5675
|
+
const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
|
|
5676
|
+
let rolloverState = parseJsonResult(rolloverResult);
|
|
5677
|
+
const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
5678
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
5679
|
+
if (
|
|
5680
|
+
rolloverState &&
|
|
5681
|
+
typeof rolloverState.revision === "number" &&
|
|
5682
|
+
rollover?.status === "prepared" &&
|
|
5683
|
+
typeof rollover.id === "string"
|
|
5684
|
+
) {
|
|
5685
|
+
pendingRollover = {
|
|
5686
|
+
snapshot: inspected.snapshot,
|
|
5687
|
+
preparedRevision: rolloverState.revision,
|
|
5688
|
+
rolloverId: rollover.id,
|
|
5689
|
+
};
|
|
5690
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
5691
|
+
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
5692
|
+
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
5299
5693
|
return;
|
|
5300
5694
|
}
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
|
|
5309
|
-
return;
|
|
5310
|
-
}
|
|
5311
|
-
if (!status || ["completed", "cancelled", "failed"].includes(status)) {
|
|
5312
|
-
ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
|
|
5313
|
-
return;
|
|
5314
|
-
}
|
|
5315
|
-
const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
|
|
5316
|
-
const confirmed = await ctx.ui.confirm(
|
|
5317
|
-
"确认弃用当前需求",
|
|
5318
|
-
`需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
|
|
5319
|
-
);
|
|
5320
|
-
if (!confirmed) {
|
|
5321
|
-
ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
|
|
5322
|
-
return;
|
|
5323
|
-
}
|
|
5324
|
-
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
5325
|
-
"--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
|
|
5326
|
-
]);
|
|
5327
|
-
if (!result.ok) {
|
|
5328
|
-
ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
|
|
5329
|
-
return;
|
|
5330
|
-
}
|
|
5331
|
-
activeContract = undefined;
|
|
5332
|
-
contractAgentCalls = 0;
|
|
5333
|
-
setControlState(ctx, initialControlState());
|
|
5334
|
-
ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
|
|
5695
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
5696
|
+
return;
|
|
5697
|
+
}
|
|
5698
|
+
if (trimmed.startsWith("start ")) {
|
|
5699
|
+
const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
|
|
5700
|
+
if (!feature) {
|
|
5701
|
+
ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
|
|
5335
5702
|
return;
|
|
5336
5703
|
}
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5704
|
+
const phase = requestedPhase || defaultPhaseId(feature);
|
|
5705
|
+
if (!validPhaseId(phase)) {
|
|
5706
|
+
ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
|
|
5707
|
+
return;
|
|
5708
|
+
}
|
|
5709
|
+
const report = await runDoctor(ctx);
|
|
5710
|
+
if (!report.ok) {
|
|
5711
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5714
|
+
const existing = await inspectWorkflow(ctx.cwd);
|
|
5715
|
+
const existingFeature = existing.snapshot?.current_feature;
|
|
5716
|
+
const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
|
|
5717
|
+
if (
|
|
5718
|
+
!previousWorkflowComplete &&
|
|
5719
|
+
typeof existingFeature === "string" &&
|
|
5720
|
+
existingFeature !== feature
|
|
5721
|
+
) {
|
|
5722
|
+
ctx.ui.notify(
|
|
5723
|
+
`当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
|
|
5724
|
+
"error",
|
|
5357
5725
|
);
|
|
5358
5726
|
return;
|
|
5359
5727
|
}
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5728
|
+
// This dry-run happens before init_workflow.rb, so a rejected feature
|
|
5729
|
+
// can never overwrite generated workflow files belonging to an active
|
|
5730
|
+
// owner or a legacy phase directory that has no trustworthy owner.
|
|
5731
|
+
const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
|
|
5732
|
+
"claim",
|
|
5733
|
+
"--feature",
|
|
5734
|
+
feature,
|
|
5735
|
+
"--phase",
|
|
5736
|
+
phase,
|
|
5737
|
+
"--flow",
|
|
5738
|
+
"feature-development",
|
|
5739
|
+
"--json",
|
|
5740
|
+
]);
|
|
5741
|
+
if (!controlPlaneSucceeded(ownership)) {
|
|
5742
|
+
ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
|
|
5363
5743
|
return;
|
|
5364
5744
|
}
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5372
|
-
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5373
|
-
ctx.ui.notify(
|
|
5374
|
-
`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`,
|
|
5375
|
-
"error",
|
|
5376
|
-
);
|
|
5377
|
-
return;
|
|
5378
|
-
}
|
|
5379
|
-
// Re-arm a durable rollover left by an interrupted runtime. An
|
|
5380
|
-
// explicit resume is the user's authorization for a fresh session to
|
|
5381
|
-
// adopt the same project's integrity-checked prepared handoff.
|
|
5382
|
-
const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
|
|
5383
|
-
const rolloverState = parseJsonResult(rolloverResult);
|
|
5384
|
-
const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
5385
|
-
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
5386
|
-
if (
|
|
5387
|
-
rolloverState &&
|
|
5388
|
-
typeof rolloverState.revision === "number" &&
|
|
5389
|
-
rollover?.status === "prepared" &&
|
|
5390
|
-
typeof rollover.id === "string"
|
|
5391
|
-
) {
|
|
5392
|
-
pendingRollover = {
|
|
5393
|
-
snapshot: inspected.snapshot,
|
|
5394
|
-
preparedRevision: rolloverState.revision,
|
|
5395
|
-
rolloverId: rollover.id,
|
|
5396
|
-
};
|
|
5397
|
-
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
5398
|
-
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
5399
|
-
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
5400
|
-
return;
|
|
5401
|
-
}
|
|
5402
|
-
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
5745
|
+
const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
|
|
5746
|
+
"--write",
|
|
5747
|
+
"--json",
|
|
5748
|
+
]);
|
|
5749
|
+
if (!controlPlaneSucceeded(workflow)) {
|
|
5750
|
+
ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
|
|
5403
5751
|
return;
|
|
5404
5752
|
}
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
return;
|
|
5434
|
-
}
|
|
5435
|
-
// This dry-run happens before init_workflow.rb, so a rejected feature
|
|
5436
|
-
// can never overwrite generated workflow files belonging to an active
|
|
5437
|
-
// owner or a legacy phase directory that has no trustworthy owner.
|
|
5438
|
-
const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
|
|
5439
|
-
"claim",
|
|
5440
|
-
"--feature",
|
|
5441
|
-
feature,
|
|
5442
|
-
"--phase",
|
|
5443
|
-
phase,
|
|
5444
|
-
"--flow",
|
|
5445
|
-
"feature-development",
|
|
5446
|
-
"--json",
|
|
5447
|
-
]);
|
|
5448
|
-
if (!controlPlaneSucceeded(ownership)) {
|
|
5449
|
-
ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
|
|
5450
|
-
return;
|
|
5451
|
-
}
|
|
5452
|
-
const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
|
|
5453
|
-
"--write",
|
|
5454
|
-
"--json",
|
|
5455
|
-
]);
|
|
5456
|
-
if (!controlPlaneSucceeded(workflow)) {
|
|
5457
|
-
ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
|
|
5458
|
-
return;
|
|
5459
|
-
}
|
|
5460
|
-
const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
|
|
5461
|
-
"--feature",
|
|
5462
|
-
feature,
|
|
5463
|
-
"--phase",
|
|
5464
|
-
phase,
|
|
5465
|
-
"--flow",
|
|
5466
|
-
"feature-development",
|
|
5467
|
-
"--write",
|
|
5468
|
-
"--json",
|
|
5469
|
-
...(previousWorkflowComplete ? ["--force"] : []),
|
|
5470
|
-
]);
|
|
5471
|
-
if (!controlPlaneSucceeded(state)) {
|
|
5472
|
-
ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
|
|
5473
|
-
return;
|
|
5474
|
-
}
|
|
5475
|
-
const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
5476
|
-
"--write",
|
|
5477
|
-
"--json",
|
|
5478
|
-
]);
|
|
5479
|
-
if (!controlPlaneSucceeded(artifacts)) {
|
|
5480
|
-
ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
|
|
5481
|
-
return;
|
|
5482
|
-
}
|
|
5483
|
-
const inspected = await inspectWorkflow(ctx.cwd);
|
|
5484
|
-
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5485
|
-
ctx.ui.notify(
|
|
5486
|
-
`Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
|
|
5487
|
-
"error",
|
|
5488
|
-
);
|
|
5489
|
-
return;
|
|
5490
|
-
}
|
|
5491
|
-
setControlState(ctx, planningState("requirement-draft"));
|
|
5492
|
-
await pi.sendUserMessage(
|
|
5493
|
-
[
|
|
5494
|
-
"<devflow-requirement-draft-directive>",
|
|
5495
|
-
`Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
|
|
5496
|
-
"请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
|
|
5497
|
-
"不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
|
|
5498
|
-
"</devflow-requirement-draft-directive>",
|
|
5499
|
-
].join("\n"),
|
|
5753
|
+
const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
|
|
5754
|
+
"--feature",
|
|
5755
|
+
feature,
|
|
5756
|
+
"--phase",
|
|
5757
|
+
phase,
|
|
5758
|
+
"--flow",
|
|
5759
|
+
"feature-development",
|
|
5760
|
+
"--write",
|
|
5761
|
+
"--json",
|
|
5762
|
+
...(previousWorkflowComplete ? ["--force"] : []),
|
|
5763
|
+
]);
|
|
5764
|
+
if (!controlPlaneSucceeded(state)) {
|
|
5765
|
+
ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
5769
|
+
"--write",
|
|
5770
|
+
"--json",
|
|
5771
|
+
]);
|
|
5772
|
+
if (!controlPlaneSucceeded(artifacts)) {
|
|
5773
|
+
ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
const inspected = await inspectWorkflow(ctx.cwd);
|
|
5777
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5778
|
+
ctx.ui.notify(
|
|
5779
|
+
`Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
|
|
5780
|
+
"error",
|
|
5500
5781
|
);
|
|
5501
5782
|
return;
|
|
5502
5783
|
}
|
|
5503
|
-
ctx
|
|
5504
|
-
|
|
5505
|
-
|
|
5784
|
+
setControlState(ctx, planningState("requirement-draft"));
|
|
5785
|
+
await pi.sendUserMessage(
|
|
5786
|
+
[
|
|
5787
|
+
"<devflow-requirement-draft-directive>",
|
|
5788
|
+
`Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
|
|
5789
|
+
"请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
|
|
5790
|
+
"不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
|
|
5791
|
+
"</devflow-requirement-draft-directive>",
|
|
5792
|
+
].join("\n"),
|
|
5506
5793
|
);
|
|
5507
|
-
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
ctx.ui.notify(
|
|
5797
|
+
"用法:/devflow(查看状态) | /devflow next(下一步) | /devflow fix(诊断修复) | /devflow cancel [原因] | /devflow control | /devflow start <需求名> | /devflow project-init | /devflow doctor\n兼容旧命令:status [--json] | explain-state | recover | abandon | resume",
|
|
5798
|
+
"warning",
|
|
5799
|
+
);
|
|
5800
|
+
};
|
|
5801
|
+
devflowCommandRef = devflowCommand;
|
|
5802
|
+
|
|
5803
|
+
pi.registerCommand("devflow", {
|
|
5804
|
+
description: "Devflow runtime: inspect, initialize, plan, run one step, or explicitly advance the controlled delivery pipeline",
|
|
5805
|
+
handler: devflowCommand,
|
|
5508
5806
|
});
|
|
5509
5807
|
}
|