@guanlin4924/devflow-pi 0.1.98 → 0.1.100

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 (26) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +635 -263
  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/init_workflow.rb +3 -1
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +14 -5
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +8 -1
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +12 -5
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +14 -1
  15. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +6 -2
  16. package/plugins/devflow-intel/extensions/devflow-intel.ts +25 -3
  17. package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +55 -28
  18. package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +41 -14
  19. package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +2 -2
  20. package/src/constants.ts +1 -1
  21. package/src/mcp/core.ts +23 -24
  22. package/src/mcp/server.ts +11 -12
  23. package/src/project-context.ts +9 -1
  24. package/src/project-initializer.ts +4 -4
  25. package/src/session-rollover.ts +10 -0
  26. package/src/workflow-diagnosis.ts +423 -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
  }
@@ -2613,7 +2672,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2613
2672
  const details = issues.length > 0
2614
2673
  ? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
2615
2674
  : "- 开工预检输出无法解析";
2616
- ctx.ui.notify(`开工预检未通过:\n${details}`, "error");
2675
+ ctx.ui.notify(`开工预检未通过:\n${details}\n可以说「下一步」,由 Devflow 诊断并给出处理办法。`, "error");
2617
2676
  return { ok: false };
2618
2677
  }
2619
2678
  const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
@@ -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,257 @@ export default function devflowCore(pi: ExtensionAPI) {
2909
2982
  return true;
2910
2983
  }
2911
2984
 
2985
+ async function diagnoseCurrentWorkflow(cwd: string, options: { skipWorkflowUpgrade?: boolean } = {}): 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
+ let workflowUpgrade = { outdated: false, blocked: false, paths: [] as string[] };
3000
+ if (typeof snapshot?.current_feature === "string" && !options.skipWorkflowUpgrade) {
3001
+ try {
3002
+ const upgradeRun = await runRuby(
3003
+ cwd,
3004
+ "workflow-orchestrator/scripts/init_workflow.rb",
3005
+ ["--force", "--json"],
3006
+ );
3007
+ if (!upgradeRun.ok) throw new Error("workflow upgrade dry-run failed");
3008
+ const upgradeResult = parseJsonResult(upgradeRun);
3009
+ if (!upgradeResult) throw new Error("workflow upgrade dry-run returned invalid JSON");
3010
+ // init_workflow.rb returns { changes: [{ path, exists,
3011
+ // same_as_template, action, ... }] }; a dry-run drift has action
3012
+ // "would_skip_existing", so drift is derived from the boolean fields.
3013
+ const changes = Array.isArray(upgradeResult?.changes) ? upgradeResult.changes : [];
3014
+ const records = changes.filter((change): change is Record<string, unknown> =>
3015
+ Boolean(change && typeof change === "object" && !Array.isArray(change))
3016
+ );
3017
+ const paths = records.flatMap(change =>
3018
+ change.exists === true && change.same_as_template !== true && typeof change.path === "string"
3019
+ ? [change.path]
3020
+ : []
3021
+ );
3022
+ workflowUpgrade = {
3023
+ outdated: paths.length > 0,
3024
+ blocked: records.some(change =>
3025
+ change.action === "blocked_state_mismatch" || change.action === "would_overwrite_blocked"
3026
+ ),
3027
+ paths,
3028
+ };
3029
+ } catch {
3030
+ workflowUpgrade = { outdated: false, blocked: false, paths: [] };
3031
+ }
3032
+ }
3033
+ const diagnosis = diagnoseWorkflow({
3034
+ inspect: { ok: Boolean(snapshot), error: inspectError },
3035
+ snapshot,
3036
+ disposition: snapshot ? workflowResumeDisposition(snapshot) : "idle",
3037
+ outputsReady: snapshot ? isReviewableWorkflowSnapshot(snapshot) : false,
3038
+ rolloverPrepared: Boolean(rolloverState && typeof rolloverState.revision === "number"
3039
+ && rollover?.status === "prepared" && typeof rollover.id === "string"),
3040
+ workflowUpgrade,
3041
+ });
3042
+ return { snapshot, inspectError, rolloverState, diagnosis };
3043
+ }
3044
+
3045
+ async function writeHelpBundle(
3046
+ ctx: DevflowCommandContext,
3047
+ current: Awaited<ReturnType<typeof diagnoseCurrentWorkflow>>,
3048
+ ): Promise<string> {
3049
+ const report = await runDoctor(ctx).catch(() => undefined);
3050
+ const createdAt = new Date().toISOString();
3051
+ const bundle = buildHelpBundle({
3052
+ version: PRODUCT_VERSION,
3053
+ createdAt,
3054
+ diagnosis: current.diagnosis,
3055
+ snapshot: current.snapshot,
3056
+ inspectError: current.inspectError,
3057
+ doctor: report?.checks,
3058
+ rollover: current.rolloverState,
3059
+ failures: recentToolFailures,
3060
+ root: ctx.cwd,
3061
+ home: homedir(),
3062
+ });
3063
+ // .devflow/runs is the git-ignored runtime output root.
3064
+ const relative = `.devflow/runs/diagnostics/help-${createdAt.replace(/[:.]/g, "-")}.json`;
3065
+ await mkdir(path.dirname(path.join(ctx.cwd, relative)), { recursive: true });
3066
+ await writeFile(path.join(ctx.cwd, relative), `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
3067
+ return relative;
3068
+ }
3069
+
3070
+ function nextStepHint(diagnosis: WorkflowDiagnosis): string {
3071
+ if (diagnosis.kind === "blocked") return "运行 /devflow fix 查看处理办法。";
3072
+ if (diagnosis.kind === "no-feature" || diagnosis.kind === "complete") return "说「下一步」或运行 /devflow start <需求名> 创建需求。";
3073
+ return "说「下一步」或运行 /devflow next 继续。";
3074
+ }
3075
+
3076
+ async function showWorkflowOverview(ctx: DevflowCommandContext): Promise<void> {
3077
+ const { diagnosis } = await diagnoseCurrentWorkflow(ctx.cwd);
3078
+ ctx.ui.notify(`${renderDiagnosis(diagnosis)}\n\n${nextStepHint(diagnosis)}`, diagnosis.kind === "blocked" ? "error" : "info");
3079
+ }
3080
+
3081
+ /** Fixes that start or advance a step; `/devflow fix` never offers them. */
3082
+ const ADVANCING_FIXES = new Set<FixAction>(["review-and-advance", "rerun-step", "resume-rollover", "create-feature"]);
3083
+
3084
+ /**
3085
+ * Single user entry: diagnose → offer a fix from the closed catalog → let
3086
+ * Core execute it → re-diagnose. The model is never asked to repair control
3087
+ * state; every write goes through the existing runtime/Core paths.
3088
+ */
3089
+ async function runNextStep(ctx: DevflowCommandContext, mode: "next" | "fix"): Promise<void> {
3090
+ if (activeWorkflowTurn || controlState.phase === "executing") {
3091
+ ctx.ui.notify("当前步骤正在执行,等这一轮结束后再说「下一步」。", "info");
3092
+ return;
3093
+ }
3094
+ if (mode === "next") {
3095
+ const report = await runDoctor(ctx);
3096
+ if (!report.ok) {
3097
+ ctx.ui.notify(`运行环境检查没有通过,暂时不能推进:\n${formatDoctorReport(report).slice(0, 6000)}`, "error");
3098
+ return;
3099
+ }
3100
+ }
3101
+ // Declining the template upgrade must not strand the step behind it.
3102
+ let skipWorkflowUpgrade = false;
3103
+ for (let attempt = 0; attempt < 4; attempt += 1) {
3104
+ const current = await diagnoseCurrentWorkflow(ctx.cwd, { skipWorkflowUpgrade });
3105
+ const { diagnosis, snapshot } = current;
3106
+ const summary = renderDiagnosis(diagnosis);
3107
+ if (diagnosis.kind === "execute" && snapshot) {
3108
+ if (mode === "fix") {
3109
+ ctx.ui.notify(`${summary}\n${nextStepHint(diagnosis)}`, "info");
3110
+ return;
3111
+ }
3112
+ await resumeInspectedWorkflow(ctx, snapshot, { doctorAlreadyPassed: true });
3113
+ return;
3114
+ }
3115
+ const fixes = diagnosis.diagnoses
3116
+ .flatMap(item => item.fixes)
3117
+ .filter(fix => mode === "next" || !ADVANCING_FIXES.has(fix));
3118
+ ctx.ui.notify(fixes.length > 0 || mode === "next" ? summary : `${summary}\n${nextStepHint(diagnosis)}`, diagnosis.kind === "blocked" ? "error" : "info");
3119
+ if (fixes.length === 0) return;
3120
+ const skip = "暂不处理";
3121
+ const choice = await ctx.ui.select("选择处理方式", [
3122
+ ...fixes.map(fix => ({ label: FIX_LABELS[fix].label, description: FIX_LABELS[fix].description })),
3123
+ { label: skip, description: "保持现状,不做任何修改" },
3124
+ ]);
3125
+ const fix = fixes.find(item => FIX_LABELS[item].label === choice);
3126
+ const upgradeOffered = diagnosis.diagnoses.some(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED");
3127
+ if (!fix && upgradeOffered) {
3128
+ skipWorkflowUpgrade = true;
3129
+ ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
3130
+ continue;
3131
+ }
3132
+ if (!fix) {
3133
+ ctx.ui.notify("已保持现状,未做任何修改。", "info");
3134
+ return;
3135
+ }
3136
+ if (fix === "create-feature") {
3137
+ const name = (await ctx.ui.input("创建需求", "需求名称,例如:私海线索转回公海保护"))?.trim();
3138
+ if (!name) {
3139
+ ctx.ui.notify("未输入需求名称,未创建需求。", "info");
3140
+ return;
3141
+ }
3142
+ await devflowCommandRef?.(`start ${name}`, ctx);
3143
+ return;
3144
+ }
3145
+ if (fix === "review-and-advance") {
3146
+ setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
3147
+ if (!(await confirmAndAdvanceWorkflowStep(ctx))) {
3148
+ ctx.ui.notify("无法打开推进确认:当前步骤的产物已经变化。请重新说「下一步」。", "error");
3149
+ }
3150
+ return;
3151
+ }
3152
+ if (fix === "revise-outputs") {
3153
+ setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
3154
+ ctx.ui.notify("已回到审查状态。请直接说明需要修改的内容;改完后说「批准执行」或「下一步」推进。", "info");
3155
+ return;
3156
+ }
3157
+ if (fix === "rerun-step" && snapshot) {
3158
+ const stepId = diagnosis.stepId ?? "当前步骤";
3159
+ setControlState(ctx, executingState("开工预检中"));
3160
+ void startWorkflowAfterPreflight(ctx, snapshot, "start", {
3161
+ doctorAlreadyPassed: true,
3162
+ executingLabel: `重新执行 ${stepId}`,
3163
+ }).catch(error => {
3164
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
3165
+ ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
3166
+ });
3167
+ return;
3168
+ }
3169
+ if (fix === "resume-rollover" && snapshot) {
3170
+ const rollover = current.rolloverState?.rollover as Record<string, unknown> | undefined;
3171
+ if (typeof current.rolloverState?.revision !== "number" || typeof rollover?.id !== "string") return;
3172
+ pendingRollover = { snapshot, preparedRevision: current.rolloverState.revision, rolloverId: rollover.id };
3173
+ await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, ctx.sessionManager.getSessionId());
3174
+ setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
3175
+ ctx.ui.notify("已恢复未完成的会话交接;当前回合结束后会切换到新会话并继续。", "info");
3176
+ return;
3177
+ }
3178
+ if (fix === "repair-registry") {
3179
+ const plan = snapshot?.recovery_plan as Record<string, unknown> | undefined;
3180
+ if (typeof plan?.expected_revision !== "number") return;
3181
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
3182
+ "--write", "--confirm", "--expected-revision", String(plan.expected_revision), "--json",
3183
+ ]);
3184
+ if (!controlPlaneSucceeded(result)) {
3185
+ ctx.ui.notify(`登记表修复失败:${describeControlFailure(result)}`, "error");
3186
+ return;
3187
+ }
3188
+ ctx.ui.notify("登记表已修复,未推进工作流。正在重新诊断……", "info");
3189
+ continue;
3190
+ }
3191
+ if (fix === "upgrade-workflow") {
3192
+ const paths = diagnosis.diagnoses
3193
+ .find(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED")?.evidence ?? [];
3194
+ const confirmed = await ctx.ui.confirm(
3195
+ "升级工作流定义",
3196
+ `将用当前版本内置模板替换以下工作流文件:\n${paths.map(item => `- ${item}`).join("\n")}\n\n这些文件中的手工修改会被替换;需求产物和 STATE 不会改变,也不会推进任何步骤。继续吗?`,
3197
+ );
3198
+ if (!confirmed) {
3199
+ skipWorkflowUpgrade = true;
3200
+ ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
3201
+ continue;
3202
+ }
3203
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
3204
+ "--write", "--force", "--json",
3205
+ ]);
3206
+ if (!controlPlaneSucceeded(result)) {
3207
+ ctx.ui.notify(`工作流升级失败:${describeControlFailure(result)}`, "error");
3208
+ return;
3209
+ }
3210
+ const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
3211
+ "--write", "--no-preserve", "--json",
3212
+ ]);
3213
+ if (!controlPlaneSucceeded(synced)) {
3214
+ ctx.ui.notify(`工作流升级后的产物同步失败:${describeControlFailure(synced)}`, "error");
3215
+ return;
3216
+ }
3217
+ activeContract = undefined;
3218
+ contractAgentCalls = 0;
3219
+ ctx.ui.notify("工作流定义已升级。正在重新诊断……", "info");
3220
+ continue;
3221
+ }
3222
+ if (fix === "help-bundle") {
3223
+ const bundlePath = await writeHelpBundle(ctx, current);
3224
+ ctx.ui.notify(`已生成求助包:${bundlePath}\n只包含诊断信息(状态摘要、校验错误、最近的工具报错),不含源码。可以把这个文件发给维护者。`, "info");
3225
+ return;
3226
+ }
3227
+ if (fix === "cancel-feature") {
3228
+ await devflowCommandRef?.("abandon", ctx);
3229
+ return;
3230
+ }
3231
+ return;
3232
+ }
3233
+ ctx.ui.notify("自动修复后仍有问题。请运行 /devflow fix 并选择「生成求助包」。", "error");
3234
+ }
3235
+
2912
3236
  type ProjectControlCommandContext = {
2913
3237
  cwd: string;
2914
3238
  models: DoctorContext["models"];
@@ -3321,7 +3645,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3321
3645
  // sole authority: only the original source session may resume a prepared
3322
3646
  // transition, and only the already-recorded target may consume a claim.
3323
3647
  const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
3324
- const rolloverState = parseJsonResult(rolloverResult);
3648
+ let rolloverState = parseJsonResult(rolloverResult);
3325
3649
  let rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
3326
3650
  const currentSessionId = ctx.sessionManager.getSessionId();
3327
3651
  if (
@@ -3334,13 +3658,14 @@ export default function devflowCore(pi: ExtensionAPI) {
3334
3658
  ]);
3335
3659
  if (recovered.ok) {
3336
3660
  const refreshed = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]));
3661
+ rolloverState = refreshed;
3337
3662
  rollover = refreshed?.rollover as Record<string, unknown> | undefined;
3338
3663
  }
3339
3664
  }
3340
3665
  if (rolloverState && typeof rolloverState.revision === "number" && rollover?.status === "prepared" && typeof rollover.id === "string") {
3341
3666
  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) {
3667
+ const preparedRevision = preparedRolloverRevision(rolloverState);
3668
+ if (workflow.result.ok && workflow.snapshot && preparedRevision !== undefined && workflow.snapshot.revision === preparedRevision) {
3344
3669
  pendingRollover = { snapshot: workflow.snapshot, preparedRevision: rolloverState.revision, rolloverId: rollover.id };
3345
3670
  await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
3346
3671
  ctx.ui.notify("已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
@@ -3507,7 +3832,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3507
3832
  if (event.source !== "interactive") return;
3508
3833
  const controlAction = classifyNaturalControlAction(event.text);
3509
3834
  const stepNote = event.text.trim();
3510
- if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/")) {
3835
+ if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/") && !classifyNextStepPhrase(stepNote)) {
3511
3836
  stepUserNotes.push(stepNote);
3512
3837
  }
3513
3838
  if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
@@ -3561,6 +3886,24 @@ export default function devflowCore(pi: ExtensionAPI) {
3561
3886
  }
3562
3887
  return { handled: true };
3563
3888
  }
3889
+ // Short phrases such as "下一步" open the same diagnose-and-fix entry as
3890
+ // /devflow next. They never approve by themselves: advancing still needs
3891
+ // the runtime confirmation dialog. "继续" is only taken over when a
3892
+ // feature is in progress so ordinary chat continuation reaches the model.
3893
+ const nextPhrase = classifyNextStepPhrase(event.text);
3894
+ if (nextPhrase && ctx.hasUI && ["idle", "waiting-user", "blocked"].includes(controlState.phase)) {
3895
+ let takeOver = nextPhrase === "next";
3896
+ if (nextPhrase === "continue") {
3897
+ const inspected = await inspectWorkflow(ctx.cwd);
3898
+ takeOver = typeof inspected.snapshot?.current_feature === "string"
3899
+ && !["completed", "cancelled", "failed"].includes(String(inspected.snapshot.status));
3900
+ }
3901
+ if (takeOver) {
3902
+ // runNextStep only uses members shared by event and command contexts.
3903
+ await runNextStep(ctx as DevflowCommandContext, "next");
3904
+ return { handled: true };
3905
+ }
3906
+ }
3564
3907
  // The in-memory prompt state is intentionally not authoritative. A fresh
3565
3908
  // session starts idle, so recover the approval arm from the durable Core
3566
3909
  // snapshot before handing an explicit approval phrase to the model. This
@@ -3596,10 +3939,8 @@ export default function devflowCore(pi: ExtensionAPI) {
3596
3939
  return { handled: true };
3597
3940
  }
3598
3941
  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
- ]);
3942
+ const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
3943
+ wireframeApprovalArgs(inspected.snapshot.current_feature, revision));
3603
3944
  if (!controlPlaneSucceeded(approval)) {
3604
3945
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
3605
3946
  ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
@@ -3615,7 +3956,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3615
3956
  ]);
3616
3957
  if (!result.ok || !controlPlaneSucceeded(result)) {
3617
3958
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
3618
- ctx.ui.notify(`未推进:${result.stderr || result.stdout}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可请求重新执行当前步骤以修复产物。`, "error");
3959
+ ctx.ui.notify(`未推进:${describeControlFailure(result)}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可以说「下一步」重新诊断,或选择重新执行这一步来修复产物。`, "error");
3619
3960
  return { handled: true };
3620
3961
  }
3621
3962
  const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
@@ -3852,6 +4193,20 @@ export default function devflowCore(pi: ExtensionAPI) {
3852
4193
  reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
3853
4194
  };
3854
4195
  }
4196
+ // Feature artifacts written while no step is running are never adopted by
4197
+ // the workflow (zhaoshang, 2026-10-22). Refuse early in the interactive
4198
+ // main session; subagents (no UI), review-feedback edits and controlled
4199
+ // turns keep their existing rules.
4200
+ if (
4201
+ ctx.hasUI &&
4202
+ controlState.phase === "idle" &&
4203
+ !activeWorkflowTurn &&
4204
+ !toolName.startsWith("devflow_") &&
4205
+ ["write", "edit", "apply_patch", "notebook_edit"].includes(toolName) &&
4206
+ inputPaths(event.input, ctx.cwd).some(filePath => planningFeatureFromPath(filePath) !== null)
4207
+ ) {
4208
+ return { block: true, reason: OUTSIDE_STEP_WRITE_REASON };
4209
+ }
3855
4210
  if (
3856
4211
  !toolName.startsWith("devflow_") &&
3857
4212
  ["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
@@ -4327,6 +4682,8 @@ export default function devflowCore(pi: ExtensionAPI) {
4327
4682
  kind: z.string().optional(),
4328
4683
  match: z.string().optional(),
4329
4684
  expected: z.unknown().optional(),
4685
+ normalize_whitespace: z.boolean().optional(),
4686
+ case_sensitive: z.boolean().optional(),
4330
4687
  name: z.string().optional(),
4331
4688
  tc_id: z.string().optional(),
4332
4689
  }),
@@ -4385,7 +4742,7 @@ export default function devflowCore(pi: ExtensionAPI) {
4385
4742
  Object.assign(requestParams, { ref: params.ref, action: params.operation, value: params.value, value_from_secret: params.value_from_secret });
4386
4743
  } else if (params.action === "assert") {
4387
4744
  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 });
4745
+ 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
4746
  } else if (params.action === "draft") {
4390
4747
  if (!params.name) return fail("draft requires name");
4391
4748
  Object.assign(requestParams, { name: params.name, tc_id: params.tc_id });
@@ -5237,273 +5594,288 @@ export default function devflowCore(pi: ExtensionAPI) {
5237
5594
  },
5238
5595
  });
5239
5596
 
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");
5597
+ const devflowCommand = async (args: string, ctx: DevflowCommandContext): Promise<void> => {
5598
+ const trimmed = args.trim();
5599
+ if (trimmed === "" || trimmed === "status") {
5600
+ await showWorkflowOverview(ctx);
5601
+ return;
5602
+ }
5603
+ if (trimmed === "status --json") {
5604
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]);
5605
+ ctx.ui.notify((result.stdout || result.stderr).slice(0, 6000), result.ok ? "info" : "error");
5606
+ return;
5607
+ }
5608
+ if (trimmed === "next" || trimmed === "下一步") {
5609
+ await runNextStep(ctx, "next");
5610
+ return;
5611
+ }
5612
+ if (trimmed === "fix") {
5613
+ await runNextStep(ctx, "fix");
5614
+ return;
5615
+ }
5616
+ if (trimmed === "control") {
5617
+ await openProjectControlPanel(ctx);
5618
+ return;
5619
+ }
5620
+ if (trimmed === "explain-state") {
5621
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5622
+ ctx.ui.notify(
5623
+ inspected.result.ok ? explainWorkflowSnapshot(inspected.snapshot) : `状态读取失败:${inspected.result.stderr || inspected.result.stdout}`,
5624
+ inspected.result.ok ? "info" : "error",
5625
+ );
5626
+ setControlState(ctx, initialControlState());
5627
+ return;
5628
+ }
5629
+ if (trimmed === "recover") {
5630
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5631
+ const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
5632
+ const revision = plan?.expected_revision;
5633
+ if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
5634
+ const status = typeof inspected.snapshot?.status === "string" ? inspected.snapshot.status : "unknown";
5635
+ const step = inspected.snapshot ? selectedWorkflowStep(inspected.snapshot) : null;
5636
+ const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
5637
+ if (inspected.result.ok && status === "in_progress") {
5638
+ ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
5639
+ } else if (inspected.result.ok && status === "waiting_checkpoint") {
5640
+ ctx.ui.notify("控制面状态一致,无需恢复。当前正在等待 checkpoint;请审查产物后明确回复“批准执行”或提出修改意见。", "info");
5641
+ } else {
5642
+ ctx.ui.notify("当前状态没有可安全自动执行的恢复计划;请运行 /devflow explain-state,并按其中的阻塞证据处理。", "error");
5643
+ }
5247
5644
  return;
5248
5645
  }
5249
- if (trimmed === "control") {
5250
- await openProjectControlPanel(ctx);
5646
+ const confirmed = await ctx.ui.confirm(
5647
+ "确认修复 Devflow 控制面",
5648
+ "将只根据已存在的文件同步 artifact registry;不会推进步骤、确认 checkpoint 或修改需求产物。继续吗?",
5649
+ );
5650
+ if (!confirmed) {
5651
+ ctx.ui.notify("恢复未确认;状态保持不变。", "info");
5251
5652
  return;
5252
5653
  }
5253
- if (trimmed === "explain-state") {
5254
- const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5654
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
5655
+ "--write",
5656
+ "--confirm",
5657
+ "--expected-revision",
5658
+ String(revision),
5659
+ "--json",
5660
+ ]);
5661
+ if (!result.ok) {
5662
+ ctx.ui.notify(`控制面恢复失败:${result.stderr || result.stdout}`, "error");
5663
+ return;
5664
+ }
5665
+ ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
5666
+ return;
5667
+ }
5668
+ if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
5669
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5670
+ const snapshot = inspected.snapshot;
5671
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
5672
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
5673
+ const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
5674
+ if (!inspected.result.ok || revision === undefined || !feature) {
5675
+ ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
5676
+ return;
5677
+ }
5678
+ if (!status || ["completed", "cancelled", "failed"].includes(status)) {
5679
+ ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
5680
+ return;
5681
+ }
5682
+ const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
5683
+ const confirmed = await ctx.ui.confirm(
5684
+ "确认弃用当前需求",
5685
+ `需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
5686
+ );
5687
+ if (!confirmed) {
5688
+ ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
5689
+ return;
5690
+ }
5691
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
5692
+ "--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
5693
+ ]);
5694
+ if (!result.ok) {
5695
+ ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
5696
+ return;
5697
+ }
5698
+ activeContract = undefined;
5699
+ contractAgentCalls = 0;
5700
+ setControlState(ctx, initialControlState());
5701
+ ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
5702
+ return;
5703
+ }
5704
+ if (trimmed === "project-init") {
5705
+ const initialization = await collectProjectInitialization(ctx);
5706
+ if (!initialization) return;
5707
+ pendingProjectInitialization = initialization;
5708
+ if (initialization.mode === "apply") {
5709
+ setControlState(ctx, executingState("执行已确认的项目初始化"));
5710
+ activeWorkflowTurn = true;
5711
+ await pi.sendUserMessage(projectInitializationDirective(initialization.summary));
5712
+ return;
5713
+ }
5714
+ setControlState(ctx, planningState("project-init"));
5715
+ await pi.sendUserMessage(
5716
+ [
5717
+ "<devflow-planner-directive>",
5718
+ "用户已确认项目初始化的输入。你现在是 Planner,只允许只读分析和派发 project-scout / requirements-auditor / contract-auditor。",
5719
+ "不得创建 .devflow/planning 文件、不得调用状态迁移、不得执行实现。",
5720
+ "先输出:已知事实、待验证点、最少侦察任务和必须由用户决定的问题。没有证据时写 unknown。",
5721
+ initialization.summary,
5722
+ "</devflow-planner-directive>",
5723
+ ].join("\n"),
5724
+ );
5725
+ return;
5726
+ }
5727
+ if (trimmed === "doctor") {
5728
+ const report = await runDoctor(ctx);
5729
+ ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), report.ok ? "info" : "error");
5730
+ return;
5731
+ }
5732
+ if (trimmed === "resume") {
5733
+ const report = await runDoctor(ctx);
5734
+ if (!report.ok) {
5735
+ ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
5736
+ return;
5737
+ }
5738
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5739
+ if (!inspected.result.ok || !inspected.snapshot) {
5255
5740
  ctx.ui.notify(
5256
- inspected.result.ok ? explainWorkflowSnapshot(inspected.snapshot) : `状态读取失败:${inspected.result.stderr || inspected.result.stdout}`,
5257
- inspected.result.ok ? "info" : "error",
5741
+ `Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`,
5742
+ "error",
5258
5743
  );
5259
- setControlState(ctx, initialControlState());
5260
5744
  return;
5261
5745
  }
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");
5746
+ // Re-arm a durable rollover left by an interrupted runtime. An
5747
+ // explicit resume is the user's authorization for a fresh session to
5748
+ // adopt the same project's integrity-checked prepared handoff.
5749
+ const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
5750
+ let rolloverState = parseJsonResult(rolloverResult);
5751
+ const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
5752
+ const currentSessionId = ctx.sessionManager.getSessionId();
5753
+ if (
5754
+ rolloverState &&
5755
+ typeof rolloverState.revision === "number" &&
5756
+ rollover?.status === "prepared" &&
5757
+ typeof rollover.id === "string"
5758
+ ) {
5759
+ pendingRollover = {
5760
+ snapshot: inspected.snapshot,
5761
+ preparedRevision: rolloverState.revision,
5762
+ rolloverId: rollover.id,
5763
+ };
5764
+ await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
5765
+ setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
5766
+ ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
5299
5767
  return;
5300
5768
  }
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");
5769
+ await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
5770
+ return;
5771
+ }
5772
+ if (trimmed.startsWith("start ")) {
5773
+ const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
5774
+ if (!feature) {
5775
+ ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
5335
5776
  return;
5336
5777
  }
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"),
5778
+ const phase = requestedPhase || defaultPhaseId(feature);
5779
+ if (!validPhaseId(phase)) {
5780
+ ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
5781
+ return;
5782
+ }
5783
+ const report = await runDoctor(ctx);
5784
+ if (!report.ok) {
5785
+ ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
5786
+ return;
5787
+ }
5788
+ const existing = await inspectWorkflow(ctx.cwd);
5789
+ const existingFeature = existing.snapshot?.current_feature;
5790
+ const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
5791
+ if (
5792
+ !previousWorkflowComplete &&
5793
+ typeof existingFeature === "string" &&
5794
+ existingFeature !== feature
5795
+ ) {
5796
+ ctx.ui.notify(
5797
+ `当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
5798
+ "error",
5357
5799
  );
5358
5800
  return;
5359
5801
  }
5360
- if (trimmed === "doctor") {
5361
- const report = await runDoctor(ctx);
5362
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), report.ok ? "info" : "error");
5802
+ // This dry-run happens before init_workflow.rb, so a rejected feature
5803
+ // can never overwrite generated workflow files belonging to an active
5804
+ // owner or a legacy phase directory that has no trustworthy owner.
5805
+ const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
5806
+ "claim",
5807
+ "--feature",
5808
+ feature,
5809
+ "--phase",
5810
+ phase,
5811
+ "--flow",
5812
+ "feature-development",
5813
+ "--json",
5814
+ ]);
5815
+ if (!controlPlaneSucceeded(ownership)) {
5816
+ ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
5363
5817
  return;
5364
5818
  }
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 });
5819
+ const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
5820
+ "--write",
5821
+ "--json",
5822
+ ]);
5823
+ if (!controlPlaneSucceeded(workflow)) {
5824
+ ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
5403
5825
  return;
5404
5826
  }
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"),
5827
+ const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
5828
+ "--feature",
5829
+ feature,
5830
+ "--phase",
5831
+ phase,
5832
+ "--flow",
5833
+ "feature-development",
5834
+ "--write",
5835
+ "--json",
5836
+ ...(previousWorkflowComplete ? ["--force"] : []),
5837
+ ]);
5838
+ if (!controlPlaneSucceeded(state)) {
5839
+ ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
5840
+ return;
5841
+ }
5842
+ const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
5843
+ "--write",
5844
+ "--json",
5845
+ ]);
5846
+ if (!controlPlaneSucceeded(artifacts)) {
5847
+ ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
5848
+ return;
5849
+ }
5850
+ const inspected = await inspectWorkflow(ctx.cwd);
5851
+ if (!inspected.result.ok || !inspected.snapshot) {
5852
+ ctx.ui.notify(
5853
+ `Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
5854
+ "error",
5500
5855
  );
5501
5856
  return;
5502
5857
  }
5503
- ctx.ui.notify(
5504
- "Usage: /devflow doctor | status | control | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
5505
- "warning",
5858
+ setControlState(ctx, planningState("requirement-draft"));
5859
+ await pi.sendUserMessage(
5860
+ [
5861
+ "<devflow-requirement-draft-directive>",
5862
+ `Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
5863
+ "请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
5864
+ "不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
5865
+ "</devflow-requirement-draft-directive>",
5866
+ ].join("\n"),
5506
5867
  );
5507
- },
5868
+ return;
5869
+ }
5870
+ ctx.ui.notify(
5871
+ "用法:/devflow(查看状态) | /devflow next(下一步) | /devflow fix(诊断修复) | /devflow cancel [原因] | /devflow control | /devflow start <需求名> | /devflow project-init | /devflow doctor\n兼容旧命令:status [--json] | explain-state | recover | abandon | resume",
5872
+ "warning",
5873
+ );
5874
+ };
5875
+ devflowCommandRef = devflowCommand;
5876
+
5877
+ pi.registerCommand("devflow", {
5878
+ description: "Devflow runtime: inspect, initialize, plan, run one step, or explicitly advance the controlled delivery pipeline",
5879
+ handler: devflowCommand,
5508
5880
  });
5509
5881
  }