@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.
Files changed (23) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +560 -262
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +6 -1
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +3 -1
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +3 -1
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_traceability.py +31 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +6 -3
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +11 -2
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +14 -5
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +12 -5
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +8 -0
  13. package/plugins/devflow-intel/extensions/devflow-intel.ts +25 -3
  14. package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +55 -28
  15. package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +41 -14
  16. package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +2 -2
  17. package/src/constants.ts +1 -1
  18. package/src/mcp/core.ts +23 -24
  19. package/src/mcp/server.ts +11 -12
  20. package/src/project-context.ts +9 -1
  21. package/src/project-initializer.ts +4 -4
  22. package/src/session-rollover.ts +10 -0
  23. 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
- const text = result.stdout || result.stderr || `Process exited with ${result.exitCode}`;
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>): boolean {
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
- && !["failed", "blocked", "planned"].includes(String(record.artifact_status ?? ""));
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.stderr || result.stdout}`, "error");
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
- const rolloverState = parseJsonResult(rolloverResult);
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 expectedStateRevision = typeof rollover.expected_state_revision === "number" ? rollover.expected_state_revision : undefined;
3343
- if (workflow.result.ok && workflow.snapshot && expectedStateRevision !== undefined && expectedStateRevision + 1 === rolloverState.revision) {
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
- "--action", "approve", "--feature", inspected.snapshot.current_feature,
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.stderr || result.stdout}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可请求重新执行当前步骤以修复产物。`, "error");
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
- pi.registerCommand("devflow", {
5241
- description: "Devflow runtime: inspect, initialize, plan, run one step, or explicitly advance the controlled delivery pipeline",
5242
- handler: async (args, ctx) => {
5243
- const trimmed = args.trim();
5244
- if (trimmed === "" || trimmed === "status") {
5245
- const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]);
5246
- ctx.ui.notify((result.stdout || result.stderr).slice(0, 6000), result.ok ? "info" : "error");
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
- if (trimmed === "control") {
5250
- await openProjectControlPanel(ctx);
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
- if (trimmed === "explain-state") {
5254
- const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
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
- inspected.result.ok ? explainWorkflowSnapshot(inspected.snapshot) : `状态读取失败:${inspected.result.stderr || inspected.result.stdout}`,
5257
- inspected.result.ok ? "info" : "error",
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
- if (trimmed === "recover") {
5263
- const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5264
- const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
5265
- const revision = plan?.expected_revision;
5266
- if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
5267
- const status = typeof inspected.snapshot?.status === "string" ? inspected.snapshot.status : "unknown";
5268
- const step = inspected.snapshot ? selectedWorkflowStep(inspected.snapshot) : null;
5269
- const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
5270
- if (inspected.result.ok && status === "in_progress") {
5271
- ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
5272
- } else if (inspected.result.ok && status === "waiting_checkpoint") {
5273
- ctx.ui.notify("控制面状态一致,无需恢复。当前正在等待 checkpoint;请审查产物后明确回复“批准执行”或提出修改意见。", "info");
5274
- } else {
5275
- ctx.ui.notify("当前状态没有可安全自动执行的恢复计划;请运行 /devflow explain-state,并按其中的阻塞证据处理。", "error");
5276
- }
5277
- return;
5278
- }
5279
- const confirmed = await ctx.ui.confirm(
5280
- "确认修复 Devflow 控制面",
5281
- "将只根据已存在的文件同步 artifact registry;不会推进步骤、确认 checkpoint 或修改需求产物。继续吗?",
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
- if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
5302
- const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5303
- const snapshot = inspected.snapshot;
5304
- const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
5305
- const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
5306
- const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
5307
- if (!inspected.result.ok || revision === undefined || !feature) {
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
- if (trimmed === "project-init") {
5338
- const initialization = await collectProjectInitialization(ctx);
5339
- if (!initialization) return;
5340
- pendingProjectInitialization = initialization;
5341
- if (initialization.mode === "apply") {
5342
- setControlState(ctx, executingState("执行已确认的项目初始化"));
5343
- activeWorkflowTurn = true;
5344
- await pi.sendUserMessage(projectInitializationDirective(initialization.summary));
5345
- return;
5346
- }
5347
- setControlState(ctx, planningState("project-init"));
5348
- await pi.sendUserMessage(
5349
- [
5350
- "<devflow-planner-directive>",
5351
- "用户已确认项目初始化的输入。你现在是 Planner,只允许只读分析和派发 project-scout / requirements-auditor / contract-auditor。",
5352
- "不得创建 .devflow/planning 文件、不得调用状态迁移、不得执行实现。",
5353
- "先输出:已知事实、待验证点、最少侦察任务和必须由用户决定的问题。没有证据时写 unknown。",
5354
- initialization.summary,
5355
- "</devflow-planner-directive>",
5356
- ].join("\n"),
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
- if (trimmed === "doctor") {
5361
- const report = await runDoctor(ctx);
5362
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), report.ok ? "info" : "error");
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
- if (trimmed === "resume") {
5366
- const report = await runDoctor(ctx);
5367
- if (!report.ok) {
5368
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
5369
- return;
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
- if (trimmed.startsWith("start ")) {
5406
- const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
5407
- if (!feature) {
5408
- ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
5409
- return;
5410
- }
5411
- const phase = requestedPhase || defaultPhaseId(feature);
5412
- if (!validPhaseId(phase)) {
5413
- ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
5414
- return;
5415
- }
5416
- const report = await runDoctor(ctx);
5417
- if (!report.ok) {
5418
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
5419
- return;
5420
- }
5421
- const existing = await inspectWorkflow(ctx.cwd);
5422
- const existingFeature = existing.snapshot?.current_feature;
5423
- const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
5424
- if (
5425
- !previousWorkflowComplete &&
5426
- typeof existingFeature === "string" &&
5427
- existingFeature !== feature
5428
- ) {
5429
- ctx.ui.notify(
5430
- `当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
5431
- "error",
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.ui.notify(
5504
- "Usage: /devflow doctor | status | control | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
5505
- "warning",
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
  }