@guanlin4924/devflow-pi 0.1.39 → 0.1.47

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 (53) hide show
  1. package/package.json +1 -1
  2. package/plugins/devflow-core/extensions/devflow-core.ts +728 -39
  3. package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +13 -0
  4. package/plugins/devflow-core/skills/wireframe-proposal/agents/openai.yaml +4 -0
  5. package/plugins/devflow-core/skills/wireframe-render/SKILL.md +8 -0
  6. package/plugins/devflow-core/skills/wireframe-review/SKILL.md +8 -0
  7. package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +8 -0
  8. package/plugins/devflow-core/skills/wireframe-validate/agents/openai.yaml +4 -0
  9. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +16 -1
  10. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-candidate-delta.schema.yaml +41 -0
  11. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-claim.schema.yaml +10 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model-policy.yaml +51 -0
  13. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model.schema.yaml +241 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-source-registry.schema.yaml +80 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/proposal.schema.yaml +44 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/questions.schema.yaml +14 -0
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +13 -3
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +334 -30
  19. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +77 -0
  20. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_closure_e2e_seed.json +516 -0
  21. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +76 -0
  22. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_impact.py +51 -0
  23. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +108 -0
  24. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +570 -0
  25. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_gate.rb +89 -0
  26. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_render.py +131 -0
  27. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1063 -0
  28. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +365 -0
  29. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +2 -0
  30. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +94 -0
  31. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +3 -3
  32. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +3 -0
  33. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +84 -6
  34. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +3 -0
  35. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +45 -12
  36. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
  37. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +248 -0
  38. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +139 -14
  39. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph_backfill.rb +143 -0
  40. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +15 -4
  41. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +45 -1
  42. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +86 -2
  43. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +222 -0
  44. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +231 -0
  45. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +457 -0
  46. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +94 -2
  47. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +111 -1
  48. package/plugins/devflow-implementation/agents/implementation-worker.md +6 -0
  49. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +107 -37
  50. package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +20 -1
  51. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +1 -1
  52. package/src/constants.ts +1 -1
  53. package/src/runtime.ts +61 -24
@@ -4,6 +4,10 @@ import { appendFile, mkdir, rename, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
7
+ import {
8
+ TASK_SUBAGENT_LIFECYCLE_CHANNEL,
9
+ type SubagentLifecyclePayload,
10
+ } from "@oh-my-pi/pi-coding-agent/task";
7
11
 
8
12
 
9
13
  export type DevflowIntent = "idle" | "explain-state" | "project-init" | "requirement-draft" | "workflow-execute";
@@ -274,14 +278,37 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
274
278
  return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
275
279
  }
276
280
 
277
- /** `xd://devflow_contract` is an in-memory OMP control resource, not a
278
- * project file. The planner must be able to read and activate it while the
279
- * filesystem remains read-only; otherwise initialization deadlocks before it
280
- * can create a bounded execution contract. */
281
- export function isInternalDevflowContractResource(toolName: string, input: unknown): boolean {
282
- if (!["read", "write"].includes(toolName.toLowerCase()) || !input || typeof input !== "object") return false;
281
+ type InternalDevflowControlResource = "contract" | "state" | "mainflow";
282
+
283
+ /** Resolve an xdev target only when all supplied target aliases agree.
284
+ * A direct/eval caller can otherwise provide `path` for a real file plus an
285
+ * unrelated `uri` alias and trick a permissive `some(...)` check. */
286
+ function unambiguousVirtualResourceTarget(input: unknown): string | undefined {
287
+ if (!input || typeof input !== "object") return undefined;
283
288
  const record = input as Record<string, unknown>;
284
- return [record.path, record.uri, record.resource].some(value => value === "xd://devflow_contract");
289
+ const targets = [record.path, record.uri, record.resource]
290
+ .filter((value): value is string => typeof value === "string" && value.length > 0);
291
+ if (targets.length === 0 || targets.some(value => value !== targets[0])) return undefined;
292
+ return targets[0];
293
+ }
294
+
295
+ /** `xd://devflow_contract` and `xd://devflow_state` are OMP transports for
296
+ * the existing named Core tools, not project-file writes. Once the outer
297
+ * transport is admitted, OMP resolves the canonical tool, validates its
298
+ * schema, applies its approval tier, and emits the native tool_call before
299
+ * executing the single-sourced handler. */
300
+ export function internalDevflowControlResource(toolName: string, input: unknown): InternalDevflowControlResource | undefined {
301
+ if (!["read", "write"].includes(toolName.toLowerCase()) || !input || typeof input !== "object") return undefined;
302
+ const target = unambiguousVirtualResourceTarget(input);
303
+ if (target === "xd://devflow_contract") return "contract";
304
+ if (target === "xd://devflow_state") return "state";
305
+ if (target === "xd://devflow_mainflow") return "mainflow";
306
+ return undefined;
307
+ }
308
+
309
+ /** Backward-compatible predicate for callers that only care about contracts. */
310
+ export function isInternalDevflowContractResource(toolName: string, input: unknown): boolean {
311
+ return internalDevflowControlResource(toolName, input) === "contract";
285
312
  }
286
313
 
287
314
  export type NaturalControlAction = "approve" | "reject";
@@ -297,6 +324,13 @@ export function isStateMutationTool(toolName: string): boolean {
297
324
  return STATE_MUTATION_TOOLS.has(toolName.toLowerCase());
298
325
  }
299
326
 
327
+ /** Generic xdev writes must not bypass the named Devflow transition guard. */
328
+ export function isVirtualStateTransitionResource(toolName: string, input: unknown): boolean {
329
+ if (toolName.toLowerCase() !== "write" || !input || typeof input !== "object") return false;
330
+ const record = input as Record<string, unknown>;
331
+ return [record.path, record.uri, record.resource].some(value => value === "xd://devflow_transition");
332
+ }
333
+
300
334
  export function planningToolBlockReason(toolName: string): string {
301
335
  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.`;
302
336
  }
@@ -448,6 +482,7 @@ const REQUIRED_SCRIPTS = [
448
482
  "workflow-orchestrator/scripts/tdd_evidence.py",
449
483
  "workflow-orchestrator/scripts/acceptance_test_runner.py",
450
484
  "workflow-orchestrator/scripts/machine_evidence.rb",
485
+ "workflow-orchestrator/scripts/cross_track_consistency_check.rb",
451
486
  "workflow-orchestrator/scripts/execution_contract.rb",
452
487
  "workflow-code-index/scripts/code_index.py",
453
488
  "workflow-context-sync/scripts/context_sync_cursor.py",
@@ -575,6 +610,11 @@ function isCoreOwnedGovernancePath(filePath: string): boolean {
575
610
  || /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath);
576
611
  }
577
612
 
613
+ function planningFeatureFromPath(filePath: string): string | null {
614
+ const match = filePath.match(/^\.planning\/(?:requirements|features)\/([^/\\\x00-\x1F\x7F]+)(?:\/|$)/u);
615
+ return match?.[1] ?? null;
616
+ }
617
+
578
618
  function resolveIntelSkillRoot(cwd: string): string | undefined {
579
619
  if (isEmbeddedRuntime()) {
580
620
  const packaged = path.join(resolvePluginRoot(), "devflow-intel", "skills");
@@ -744,6 +784,67 @@ function compactStep(value: unknown): Record<string, unknown> | null {
744
784
  };
745
785
  }
746
786
 
787
+ /**
788
+ * The approval prompt is session-local, while the completed step outputs are
789
+ * durable. After a rollover, compaction, or runtime restart the in-memory
790
+ * control state can be idle even though the user is still looking at a
791
+ * completed step. This predicate lets the runtime re-arm approval handling
792
+ * from the Core-owned snapshot without granting approval by itself.
793
+ */
794
+ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolean {
795
+ const status = snapshot.status;
796
+ if (status !== "waiting_checkpoint" && status !== "not_started" && status !== "in_progress") return false;
797
+ const step = snapshot.current_step;
798
+ if (!step || typeof step !== "object") return false;
799
+ const stepRecord = step as Record<string, unknown>;
800
+ if (typeof stepRecord.id !== "string" || !stepRecord.id) return false;
801
+ const outputs = Array.isArray(stepRecord.outputs) ? stepRecord.outputs : [];
802
+ const requiredOutputs = outputs.filter(output => {
803
+ if (!output || typeof output !== "object") return false;
804
+ return (output as Record<string, unknown>).required !== false;
805
+ });
806
+ return requiredOutputs.length > 0 && requiredOutputs.every(output => {
807
+ if (!output || typeof output !== "object") return false;
808
+ const record = output as Record<string, unknown>;
809
+ return record.exists === true && !["failed", "blocked"].includes(String(record.artifact_status ?? ""));
810
+ });
811
+ }
812
+
813
+ export type WorkflowResumeDisposition = "execute" | "review" | "blocked" | "complete" | "idle";
814
+
815
+ /** Decide how an explicit workflow resume should re-enter the Runtime.
816
+ *
817
+ * Core STATE is authoritative, but output presence determines whether the
818
+ * current step is ready for review or still needs an agent turn. In
819
+ * particular, `run_next_step` does not mean that an existing current step is
820
+ * complete: after a process restart its required output may still be absent.
821
+ */
822
+ export function workflowResumeDisposition(snapshot: Record<string, unknown>): WorkflowResumeDisposition {
823
+ const action = typeof snapshot.action === "string" ? snapshot.action : "";
824
+ const status = typeof snapshot.status === "string" ? snapshot.status : "";
825
+ if (["workflow_complete", "workflow_cancelled", "workflow_failed"].includes(action) || ["completed", "cancelled", "failed"].includes(status)) {
826
+ return "complete";
827
+ }
828
+ if (action === "blocked") return "blocked";
829
+
830
+ const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
831
+ const revision = snapshot.revision;
832
+ const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
833
+ const stepId = typeof selectedStep?.id === "string" ? selectedStep.id : "";
834
+ if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
835
+
836
+ if (action === "await_user_confirmation") {
837
+ return isReviewableWorkflowSnapshot(snapshot) ? "review" : "blocked";
838
+ }
839
+ // Parallel windows must be reconstructed by the Runtime even if a stale
840
+ // join artifact happens to exist; branch lifecycle is the join authority.
841
+ if (action === "run_parallel_window" || action === "route_failure_branch") return "execute";
842
+ if (action === "run_next_step") {
843
+ return isReviewableWorkflowSnapshot(snapshot) ? "review" : "execute";
844
+ }
845
+ return "blocked";
846
+ }
847
+
747
848
  function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
748
849
  const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
749
850
  const envelope = {
@@ -760,12 +861,24 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
760
861
  recovery: snapshot.recovery,
761
862
  requirement_draft: requirementDraft,
762
863
  };
864
+ const parallelInstructions = snapshot.action === "run_parallel_window"
865
+ ? [
866
+ "这是并行窗口:先按 parallel_run 中的 branch 记录逐个调用 task;一个 task 只能负责一个 workflow_step,不得把多个 step 合并到一个子 Agent。",
867
+ "本窗口的 branch step(backend-tech-spec、api-call-guide、ui-spec、feature-breakdown、frontend-tech-spec、api-supplement)统一使用白名单 agent implementation-worker;不要把 workflow step 名称当作 agent 名称。",
868
+ "只有执行验证类步骤才使用 verification-runner;project-scout、requirements-auditor、contract-auditor、devflow-code-reviewer 不属于本并行窗口的默认执行 agent。",
869
+ "每个 task 的提示必须原样包含 DEVFLOW_BRANCH_ID、DEVFLOW_WORKFLOW_STEP、DEVFLOW_TRACK、DEVFLOW_ATTEMPT 和 DEVFLOW_BRANCH_REVISION,并只写该分支的白名单产物。",
870
+ "子 Agent 完成后只返回产物和验证证据;不得调用 devflow_transition,不得自行声明 checkpoint 或 join。主 Agent 负责根据生命周期结果继续调度,Core 负责最终门禁。",
871
+ "如果生命周期 status=aborted,按 failed 处理:先调用 Core 的 branch fail,再根据恢复信息调用 retry;不得把 aborted 当作成功或忽略,否则 join 会永久等待。",
872
+ ]
873
+ : [];
763
874
  return [
764
875
  "<devflow-runtime-directive>",
765
876
  "Devflow 已通过启动预检。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
766
- "先调用 devflow_state(action=\"inspect\") 重新确认 revision,再由 workflow-orchestrator 路由当前步骤。",
767
- "只按 inspector 返回的当前 step 加载所需 Skill;不得调用 devflow_transition、不得推进下一步骤、不得替用户通过人工 checkpoint。",
768
- "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
877
+ "先调用 devflow_state(action=\"inspect\") 重新确认 revision,再调用 devflow_contract(action=\"status\");若没有匹配当前 feature/current_step 的 fresh contract,立即调用 devflow_contract(action=\"activate\", feature=<current_feature>, step=<current_step>),激活成功后才能执行或写入。",
878
+ "只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
879
+ ...(parallelInstructions.length > 0 ? parallelInstructions : [
880
+ "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
881
+ ]),
769
882
  "当前步骤信封:",
770
883
  JSON.stringify(envelope, null, 2),
771
884
  "</devflow-runtime-directive>",
@@ -820,6 +933,27 @@ function inputPaths(input: unknown, cwd: string): string[] {
820
933
  .filter((value): value is string => value !== null);
821
934
  }
822
935
 
936
+ function parallelBranchContext(input: unknown): { branch_id: string; attempt: number; branch_revision: number } | null {
937
+ const text = typeof input === "string" ? input : JSON.stringify(input ?? {});
938
+ const branch = text.match(/DEVFLOW_BRANCH_ID\s*[:=]\s*([^\s,}"']+)/)?.[1];
939
+ const attempt = text.match(/DEVFLOW_ATTEMPT\s*[:=]\s*(\d+)/)?.[1];
940
+ const revision = text.match(/DEVFLOW_BRANCH_REVISION\s*[:=]\s*(\d+)/)?.[1];
941
+ if (!branch || attempt === undefined || revision === undefined) return null;
942
+ return { branch_id: branch, attempt: Number(attempt), branch_revision: Number(revision) };
943
+ }
944
+
945
+ export function taskResultHasSuccessfulDispatch(details: unknown): boolean {
946
+ if (!details || typeof details !== "object") return false;
947
+ const record = details as Record<string, unknown>;
948
+ if (Array.isArray(record.results) && record.results.length > 0) return true;
949
+ if (!Array.isArray(record.progress)) return false;
950
+ return record.progress.some(item => {
951
+ if (!item || typeof item !== "object") return false;
952
+ const status = String((item as Record<string, unknown>).status ?? "");
953
+ return status !== "failed" && status !== "aborted";
954
+ });
955
+ }
956
+
823
957
  function isControlPath(filePath: string): boolean {
824
958
  return CONTROL_PATHS.includes(filePath) || CONTROL_PREFIXES.some(prefix => filePath.startsWith(prefix));
825
959
  }
@@ -829,7 +963,7 @@ function isDirectControlMutation(toolName: string, input: unknown, cwd: string):
829
963
  return inputPaths(input, cwd).some(isControlPath);
830
964
  }
831
965
 
832
- function isShellControlMutation(toolName: string, input: unknown): boolean {
966
+ export function isShellControlMutation(toolName: string, input: unknown): boolean {
833
967
  if (!["bash", "shell", "exec_command"].includes(toolName) || !input || typeof input !== "object") {
834
968
  return false;
835
969
  }
@@ -840,7 +974,7 @@ function isShellControlMutation(toolName: string, input: unknown): boolean {
840
974
  command,
841
975
  );
842
976
  const namesControlPath = [...CONTROL_PATHS, ...CONTROL_PREFIXES].some(item => command.includes(item)) || /\.planning\/requirements\//.test(command);
843
- const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|>{1,2}/.test(
977
+ const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|(?:^|[\s;&|])(?:\d*)>{1,2}(?![>=])\s*\S/.test(
844
978
  command,
845
979
  );
846
980
  return invokesWriter || (namesControlPath && looksLikeWrite);
@@ -858,8 +992,108 @@ function shellMutationPaths(command: string, cwd: string): { mutating: boolean;
858
992
  return { mutating: true, paths };
859
993
  }
860
994
 
995
+ type StepContractAuthority = Pick<ActiveExecutionContract, "feature" | "step">;
996
+
997
+ const CONTRACT_CONTROL_TOOLS = new Set([
998
+ "devflow_artifact",
999
+ "devflow_bootstrap",
1000
+ "devflow_contract",
1001
+ "devflow_recovery",
1002
+ "devflow_transition",
1003
+ ]);
1004
+
1005
+ const CONTRACT_READ_TOOLS = new Set([
1006
+ "devflow_acceptance_tests",
1007
+ "devflow_doctor",
1008
+ "devflow_state",
1009
+ "devflow_usage_stats",
1010
+ "usage_stats",
1011
+ ]);
1012
+
1013
+ function inputAction(input: unknown): string {
1014
+ return input && typeof input === "object" ? String((input as Record<string, unknown>).action ?? "") : "";
1015
+ }
1016
+
1017
+ function stepScopedMutationOwner(toolName: string, input: unknown): string | null | undefined {
1018
+ const normalized = toolName.toLowerCase();
1019
+ // `write xd://devflow_*` is only the OMP transport. The nested canonical
1020
+ // tool_call is authorized below under its real `devflow_*` name.
1021
+ if (internalDevflowControlResource(normalized, input)) return undefined;
1022
+ if (["write", "edit", "apply_patch", "notebook_edit", "task", "bash", "shell", "exec_command"].includes(normalized)) return null;
1023
+ if (!normalized.startsWith("devflow_") || CONTRACT_CONTROL_TOOLS.has(normalized) || CONTRACT_READ_TOOLS.has(normalized)) return undefined;
1024
+ const action = inputAction(input);
1025
+ if (normalized === "devflow_code_index") return action === "scan" ? "code-index-refresh" : undefined;
1026
+ if (normalized === "devflow_requirement_graph") {
1027
+ const writeContext = input && typeof input === "object" && (input as Record<string, unknown>).write === true;
1028
+ return ["propose", "confirm"].includes(action) || (action === "context" && writeContext) ? null : undefined;
1029
+ }
1030
+ if (normalized === "devflow_api_test_matrix") return action === "validate" ? undefined : "acceptance-test-design";
1031
+ if (normalized === "devflow_tdd_evidence") return action === "inspect" ? undefined : "execute";
1032
+ if (normalized === "devflow_run_acceptance_tests") return action === "run" ? "run-acceptance-tests" : undefined;
1033
+ if (normalized === "devflow_mainflow") return "generate-mainflow-cases";
1034
+ if (normalized === "devflow_wireframe" || normalized === "devflow_design_rule") return "ui-spec";
1035
+ if (normalized.startsWith("devflow_architecture_") || normalized === "devflow_workload_constraint") {
1036
+ return action === "inspect" ? undefined : "architecture-feasibility";
1037
+ }
1038
+ if (normalized === "devflow_runtime_context" || normalized === "devflow_static_runtime_context") {
1039
+ return action === "inspect" ? undefined : null;
1040
+ }
1041
+ if (normalized === "devflow_evidence") return ["read", "validate"].includes(action) ? undefined : null;
1042
+ if (normalized === "devflow_machine_evidence") {
1043
+ const step = input && typeof input === "object" ? (input as Record<string, unknown>).step : undefined;
1044
+ return typeof step === "string" && step ? step : null;
1045
+ }
1046
+ // New mutating Devflow tools must declare a step above or be explicitly
1047
+ // classified read/control-only; they never inherit a broad devflow_* bypass.
1048
+ return null;
1049
+ }
1050
+
1051
+ function snapshotStepId(snapshot: Record<string, unknown>): string | undefined {
1052
+ const step = snapshot.current_step;
1053
+ if (typeof step === "string" && step) return step;
1054
+ if (step && typeof step === "object" && typeof (step as Record<string, unknown>).id === "string") {
1055
+ return String((step as Record<string, unknown>).id);
1056
+ }
1057
+ return undefined;
1058
+ }
1059
+
1060
+ export function contractActivationPhaseAllowed(phase: ControlPhase, activeWorkflowTurn: boolean): boolean {
1061
+ return ["planning", "waiting-user", "awaiting-approval"].includes(phase) || (phase === "executing" && activeWorkflowTurn);
1062
+ }
1063
+
1064
+ export function stepScopedMutationBlockReason(
1065
+ toolName: string,
1066
+ input: unknown,
1067
+ _cwd: string,
1068
+ snapshot: Record<string, unknown>,
1069
+ contract: StepContractAuthority | undefined,
1070
+ contractFresh: boolean,
1071
+ ): string | undefined {
1072
+ const owner = stepScopedMutationOwner(toolName, input);
1073
+ if (owner === undefined) return undefined;
1074
+ const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : undefined;
1075
+ const step = snapshotStepId(snapshot);
1076
+ if (!feature || !step) return "Active Devflow workflow mutation blocked: authoritative feature/current_step is unavailable.";
1077
+ if (snapshot.status === "waiting_checkpoint") {
1078
+ return `Active Devflow workflow is waiting at a checkpoint after ${step}; approve or reject it before any further mutation.`;
1079
+ }
1080
+ if (!contract) return `Workflow step ${feature}/${step} requires an active execution contract before ${toolName}.`;
1081
+ if (contract.feature !== feature || contract.step !== step) {
1082
+ return `Active contract ${contract.feature}/${contract.step} does not match authoritative workflow step ${feature}/${step}.`;
1083
+ }
1084
+ if (!contractFresh) return `Active contract for ${feature}/${step} is stale against authoritative STATE or requirement graph.`;
1085
+ if (owner && owner !== step) return `${toolName} belongs to workflow step ${owner}, but authoritative current_step is ${step}.`;
1086
+ return undefined;
1087
+ }
1088
+
861
1089
  export default function devflowCore(pi: ExtensionAPI) {
862
1090
  const { z } = pi.zod;
1091
+ // CLI --resume means "continue this chat session" at the OMP layer. The
1092
+ // launcher also supplies this one-shot, process-owned signal so Devflow can
1093
+ // safely resume an authoritative incomplete workflow step. It is consumed
1094
+ // during extension construction and never becomes model-controlled state.
1095
+ const cliSessionResumeRequested = process.env.DEVFLOW_CLI_SESSION_RESUME === "1";
1096
+ delete process.env.DEVFLOW_CLI_SESSION_RESUME;
863
1097
  let sessionId = randomUUID();
864
1098
  let controlState: SessionControlState = initialControlState();
865
1099
  let activeContract: ActiveExecutionContract | undefined;
@@ -872,7 +1106,10 @@ export default function devflowCore(pi: ExtensionAPI) {
872
1106
  const step = activeContract.step;
873
1107
  if (!/^[\p{L}\p{N}._-]{1,160}$/u.test(feature) || !/^[\p{L}\p{N}._-]{1,160}$/u.test(step)) return;
874
1108
  const directory = path.join(cwd, ".planning", "evidence", feature);
875
- const destination = path.join(directory, `${step}.engine-events.jsonl`);
1109
+ const attempt = typeof extra.attempt === "number" && Number.isInteger(extra.attempt) && extra.attempt >= 0
1110
+ ? extra.attempt
1111
+ : undefined;
1112
+ const destination = path.join(directory, `${step}${attempt === undefined ? "" : `.attempt-${attempt}`}.engine-events.jsonl`);
876
1113
  await mkdir(directory, { recursive: true });
877
1114
  await appendFile(destination, `${JSON.stringify({
878
1115
  schema_version: 1,
@@ -881,6 +1118,7 @@ export default function devflowCore(pi: ExtensionAPI) {
881
1118
  feature,
882
1119
  step,
883
1120
  event,
1121
+ ...(attempt === undefined ? {} : { attempt, branch_id: extra.branch_id }),
884
1122
  collector: "devflow-runtime/harness",
885
1123
  ...extra,
886
1124
  })}\n`, "utf8");
@@ -1323,8 +1561,19 @@ export default function devflowCore(pi: ExtensionAPI) {
1323
1561
  }
1324
1562
 
1325
1563
  let activeWorkflowTurn = false;
1564
+ let parallelRunActive = false;
1565
+ let parallelContinuationQueued = false;
1566
+ let parallelDispatchSucceededThisTurn = false;
1567
+ let parallelDispatchFailureThisTurn = "";
1568
+ const pendingParallelLifecycle = new Map<string, string>();
1569
+ const pendingParallelLifecycleContexts = new Map<string, { branch_id: string; attempt: number; branch_revision: number }>();
1570
+ const parallelTaskContexts = new Map<string, { branch_id: string; attempt: number; branch_revision: number }>();
1326
1571
  let pendingProjectInitialization: ProjectInitializationRequest | undefined;
1327
1572
  let pendingRollover: { snapshot: Record<string, unknown>; preparedRevision: number; rolloverId: string } | undefined;
1573
+ // A planner may request bootstrap, but only the runtime may authorize the
1574
+ // state mutation. Keep the authorization in memory and bind it to exactly
1575
+ // one tool call; never persist or expose it as a model-controlled flag.
1576
+ const runtimeBootstrapAuthorizations = new Set<string>();
1328
1577
 
1329
1578
  async function enqueueControlledRolloverRequest(cwd: string, rolloverId: string, sourceSessionId: string) {
1330
1579
  const requestPath = path.join(cwd, ".planning", "rollover-request.json");
@@ -1347,15 +1596,258 @@ export default function devflowCore(pi: ExtensionAPI) {
1347
1596
  const rollover = payload.rollover as Record<string, unknown> | undefined;
1348
1597
  const rolloverId = typeof rollover?.id === "string" ? rollover.id : undefined;
1349
1598
  if (!rolloverId) throw new Error("rollover prepare returned no rollover id");
1599
+ const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
1600
+ "--write", "--no-preserve", "--json",
1601
+ ]);
1602
+ if (!controlPlaneSucceeded(synced)) {
1603
+ throw new Error(synced.stderr || synced.stdout || "artifact registry sync after rollover prepare failed");
1604
+ }
1350
1605
  await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
1351
1606
  pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
1352
1607
  }
1353
1608
 
1609
+ function parallelBranches(snapshot: Record<string, unknown>): Array<{ workflow_step: string; track: string }> {
1610
+ const window = snapshot.parallel_window;
1611
+ if (!window || typeof window !== "object") return [];
1612
+ const branches = (window as Record<string, unknown>).branches;
1613
+ if (!branches || typeof branches !== "object") return [];
1614
+ return Object.entries(branches as Record<string, unknown>).flatMap(([track, steps]) => {
1615
+ if (!Array.isArray(steps)) return [];
1616
+ return steps.flatMap(step => {
1617
+ const workflowStep = typeof step === "string"
1618
+ ? step
1619
+ : step && typeof step === "object" ? String((step as Record<string, unknown>).id ?? "") : "";
1620
+ return workflowStep ? [{ workflow_step: workflowStep, track }] : [];
1621
+ });
1622
+ });
1623
+ }
1624
+
1625
+ async function prepareParallelRun(cwd: string, snapshot: Record<string, unknown>): Promise<Record<string, unknown>> {
1626
+ if (snapshot.action !== "run_parallel_window") return snapshot;
1627
+ const feature = String(snapshot.current_feature ?? "");
1628
+ const window = snapshot.parallel_window as Record<string, unknown> | undefined;
1629
+ const workflowWindow = String(window?.id ?? "");
1630
+ const existing = parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/parallel_run.rb", [
1631
+ "--action", "inspect", "--feature", feature, "--json",
1632
+ ]));
1633
+ if (existing?.ok === true && existing.run) {
1634
+ parallelRunActive = true;
1635
+ return { ...snapshot, parallel_run: existing.run };
1636
+ }
1637
+ const revision = typeof snapshot.revision === "number" ? snapshot.revision : undefined;
1638
+ if (!feature || !workflowWindow || revision === undefined) throw new Error("parallel window snapshot is incomplete");
1639
+ const branches = parallelBranches(snapshot);
1640
+ if (branches.length === 0) throw new Error("parallel window contains no branch steps");
1641
+ const prepared = await runRuby(cwd, "workflow-orchestrator/scripts/parallel_run.rb", [
1642
+ "--action", "prepare", "--feature", feature, "--workflow-window", workflowWindow,
1643
+ "--state-revision", String(revision), "--json",
1644
+ ...branches.flatMap(branch => ["--branch", `${branch.workflow_step}:${branch.track}`]),
1645
+ ]);
1646
+ const payload = parseJsonResult(prepared);
1647
+ if (!controlPlaneSucceeded(prepared) || !payload?.run) {
1648
+ throw new Error(prepared.stderr || prepared.stdout || "parallel run prepare failed");
1649
+ }
1650
+ parallelRunActive = true;
1651
+ return { ...snapshot, parallel_run: payload.run };
1652
+ }
1653
+
1354
1654
  async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string) {
1655
+ if (snapshot.action !== "run_parallel_window") {
1656
+ parallelRunActive = false;
1657
+ parallelContinuationQueued = false;
1658
+ }
1659
+ if (snapshot.action === "run_parallel_window") {
1660
+ parallelDispatchSucceededThisTurn = false;
1661
+ parallelDispatchFailureThisTurn = "";
1662
+ }
1663
+ const preparedSnapshot = await prepareParallelRun(projectRoot ?? process.cwd(), snapshot);
1355
1664
  activeWorkflowTurn = true;
1356
- await pi.sendUserMessage(workflowDirective(snapshot, source, requirementDraft));
1665
+ await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft));
1666
+ }
1667
+
1668
+ async function resumeInspectedWorkflow(
1669
+ ctx: {
1670
+ cwd: string;
1671
+ ui: {
1672
+ notify(message: string, level: "info" | "error"): void;
1673
+ setStatus(key: string, text: string | undefined): void;
1674
+ setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
1675
+ };
1676
+ },
1677
+ snapshot: Record<string, unknown>,
1678
+ ): Promise<WorkflowResumeDisposition> {
1679
+ const disposition = workflowResumeDisposition(snapshot);
1680
+ const stepId = String((compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step))?.id ?? "当前步骤");
1681
+ if (disposition === "execute") {
1682
+ setControlState(ctx, executingState(`恢复并执行 ${stepId}`));
1683
+ ctx.ui.notify(`Devflow 已恢复到 ${stepId},当前步骤产物尚未完成,正在继续执行。`, "info");
1684
+ await triggerWorkflow(snapshot, "start");
1685
+ return disposition;
1686
+ }
1687
+ if (disposition === "review") {
1688
+ setControlState(ctx, waitingUserState("等待用户审查步骤产物"));
1689
+ ctx.ui.notify(`Devflow 已恢复到 ${stepId};请审查当前产物后明确回复“批准执行”,或直接提出修改意见。`, "info");
1690
+ return disposition;
1691
+ }
1692
+ if (disposition === "blocked") {
1693
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "恢复校验阻塞" });
1694
+ ctx.ui.notify(`Devflow 无法安全恢复 ${stepId}:${String(snapshot.reason ?? "当前状态既不可执行也不可审查")}。`, "error");
1695
+ return disposition;
1696
+ }
1697
+ setControlState(ctx, initialControlState());
1698
+ ctx.ui.notify(
1699
+ disposition === "complete" ? "Devflow workflow is already complete." : "当前没有可恢复的 Devflow 工作流步骤。",
1700
+ "info",
1701
+ );
1702
+ return disposition;
1357
1703
  }
1358
1704
 
1705
+ async function confirmAndAdvanceWorkflowStep(ctx: {
1706
+ cwd: string;
1707
+ sessionManager: { getSessionId(): string };
1708
+ ui: { confirm(title: string, body: string): Promise<boolean>; notify(message: string, level: "info" | "error"): void; setStatus(key: string, text: string | undefined): void };
1709
+ }): Promise<boolean> {
1710
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1711
+ if (!inspected.result.ok || !inspected.snapshot || !isReviewableWorkflowSnapshot(inspected.snapshot)) return false;
1712
+ const step = compactStep(inspected.snapshot.current_step);
1713
+ const stepId = typeof step?.id === "string" ? step.id : undefined;
1714
+ const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
1715
+ if (!stepId || revision === undefined) return false;
1716
+ const checkpoint = inspected.snapshot.status === "waiting_checkpoint";
1717
+ let cp3Prerequisites: Array<{ id?: unknown; kind?: unknown; summary?: unknown }> = [];
1718
+ if (checkpoint && inspected.snapshot.checkpoint === "CP-3") {
1719
+ const feature = typeof inspected.snapshot.current_feature === "string" ? inspected.snapshot.current_feature : undefined;
1720
+ if (!feature) return false;
1721
+ const suite = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/acceptance_test_suite.rb", [
1722
+ "inspect", "--feature", feature, "--json",
1723
+ ]);
1724
+ const suitePayload = parseJsonResult(suite);
1725
+ if (!suite.ok || !suitePayload || suitePayload.ok !== true) {
1726
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "CP-3 前置检查失败" });
1727
+ ctx.ui.notify(`无法读取 CP-3 测试前置事实:${suite.stderr || suite.stdout}`, "error");
1728
+ return true;
1729
+ }
1730
+ cp3Prerequisites = Array.isArray(suitePayload.unconfirmed_prerequisites)
1731
+ ? suitePayload.unconfirmed_prerequisites as Array<{ id?: unknown; kind?: unknown; summary?: unknown }>
1732
+ : [];
1733
+ }
1734
+ const prerequisiteReview = cp3Prerequisites.length > 0
1735
+ ? [
1736
+ "",
1737
+ `CP-3 检测到 ${cp3Prerequisites.length} 个尚未确认、但被候选验收测试引用的前置事实。批准后 Core 会在同一原子事务中确认它们并冻结测试集:`,
1738
+ ...cp3Prerequisites.map(item => `- ${String(item.id ?? "?")} [${String(item.kind ?? "?")}]${item.summary ? `:${String(item.summary)}` : ""}`),
1739
+ ].join("\n")
1740
+ : "";
1741
+ const approved = await ctx.ui.confirm(
1742
+ "确认推进 Devflow 工作流",
1743
+ `当前步骤:${stepId}\n\n步骤产物已生成并通过基础校验。确认${checkpoint ? "当前 checkpoint 并" : ""}推进到下一步骤吗?${prerequisiteReview}`,
1744
+ );
1745
+ if (!approved) {
1746
+ setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
1747
+ ctx.ui.notify("已保留当前步骤,未推进工作流。", "info");
1748
+ return true;
1749
+ }
1750
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1751
+ "--write", "--json", "--expected-revision", String(revision),
1752
+ ...(checkpoint ? ["--confirm-checkpoint", "--runtime-confirmed"] : ["--complete-step", stepId]),
1753
+ ...(cp3Prerequisites.length > 0 ? ["--confirm-test-prerequisites"] : []),
1754
+ ]);
1755
+ if (!result.ok || !controlPlaneSucceeded(result)) {
1756
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
1757
+ ctx.ui.notify(`未推进:${result.stderr || result.stdout}`, "error");
1758
+ return true;
1759
+ }
1760
+ const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1761
+ activeContract = undefined;
1762
+ contractAgentCalls = 0;
1763
+ if (!after.result.ok || !after.snapshot || after.snapshot.action === "workflow_complete") {
1764
+ setControlState(ctx, initialControlState());
1765
+ ctx.ui.notify(after.snapshot?.action === "workflow_complete" ? "工作流已完成。" : "已推进,但读取下一步骤失败。", "info");
1766
+ return true;
1767
+ }
1768
+ setControlState(ctx, waitingUserState("正在准备干净会话以执行下一工作流步骤"));
1769
+ try {
1770
+ await requestControlledRollover(ctx, after.snapshot);
1771
+ ctx.ui.notify("当前节点的证据已固定;当前回合结束后将自动切换到下一节点。", "info");
1772
+ } catch (error) {
1773
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "会话轮换准备失败" });
1774
+ ctx.ui.notify(`未开始下一节点:会话轮换准备失败:${String(error)}`, "error");
1775
+ }
1776
+ return true;
1777
+ }
1778
+
1779
+ async function reconcileParallelLifecycle(event: Partial<SubagentLifecyclePayload>, context: { branch_id: string; attempt: number; branch_revision: number } | undefined): Promise<void> {
1780
+ if (!context || !projectRoot || !event.status || event.status === "started") return;
1781
+ const workflowInspection = await inspectWorkflow(projectRoot);
1782
+ const snapshot = workflowInspection.snapshot;
1783
+ const feature = String(snapshot?.current_feature ?? "");
1784
+ if (!feature) return;
1785
+ const inspected = parseJsonResult(await runRuby(projectRoot, "workflow-orchestrator/scripts/parallel_run.rb", ["--action", "inspect", "--feature", feature, "--json"]));
1786
+ const run = inspected?.run as Record<string, unknown> | undefined;
1787
+ if (!run) return;
1788
+ const stateRevision = Number(snapshot?.revision);
1789
+ if (!Number.isInteger(stateRevision)) return;
1790
+ const action = event.status === "completed" ? "complete" : "fail";
1791
+ const args = ["--write", "--json", "--parallel-action", action, "--parallel-branch-id", context.branch_id, "--expected-revision", String(stateRevision)];
1792
+ if (action === "complete") args.push("--parallel-attempt", String(context.attempt), "--parallel-branch-revision", String(context.branch_revision));
1793
+ const result = await runRuby(projectRoot, "workflow-orchestrator/scripts/advance_state.rb", args);
1794
+ if (!result.ok || !controlPlaneSucceeded(result)) {
1795
+ parallelDispatchFailureThisTurn = result.stderr || result.stdout || `parallel ${action} failed`;
1796
+ return;
1797
+ }
1798
+ if (action !== "complete") return;
1799
+ const refreshed = parseJsonResult(await runRuby(projectRoot, "workflow-orchestrator/scripts/parallel_run.rb", ["--action", "inspect", "--feature", feature, "--json"]));
1800
+ const current = refreshed?.run as Record<string, unknown> | undefined;
1801
+ const branches = Array.isArray(current?.branches) ? current.branches as Array<Record<string, unknown>> : [];
1802
+ if (branches.length > 0 && branches.every(branch => branch.status === "succeeded")) {
1803
+ const after = await inspectWorkflow(projectRoot);
1804
+ const joinRevision = Number(after.snapshot?.revision);
1805
+ if (Number.isInteger(joinRevision)) {
1806
+ await runRuby(projectRoot, "workflow-orchestrator/scripts/advance_state.rb", [
1807
+ "--write", "--json", "--parallel-action", "join", "--expected-revision", String(joinRevision),
1808
+ ]);
1809
+ }
1810
+ }
1811
+ }
1812
+
1813
+ function scheduleParallelContinuation() {
1814
+ if (parallelContinuationQueued || pendingParallelLifecycle.size === 0) return;
1815
+ parallelContinuationQueued = true;
1816
+ const events = [...pendingParallelLifecycle.entries()].map(([id, status]) => {
1817
+ const context = pendingParallelLifecycleContexts.get(id);
1818
+ return context ? `${id}:${status}:${JSON.stringify(context)}` : `${id}:${status}`;
1819
+ });
1820
+ pendingParallelLifecycle.clear();
1821
+ pendingParallelLifecycleContexts.clear();
1822
+ pi.sendMessage({
1823
+ customType: "devflow-parallel-lifecycle",
1824
+ content: `并行子 Agent 生命周期事件已到达(${events.join(", ")})。请重新 inspect 整个 parallel run,逐一处理所有已完成/失败分支,必要时调用 Core 的 branch complete/fail/retry 动作,然后继续派发仍满足前置依赖的分支;不得直接 join。`,
1825
+ display: false,
1826
+ details: { events },
1827
+ }, { triggerTurn: true, deliverAs: "nextTurn" });
1828
+ }
1829
+
1830
+ pi.events.on(TASK_SUBAGENT_LIFECYCLE_CHANNEL, async data => {
1831
+ if (!parallelRunActive || !data || typeof data !== "object") return;
1832
+ const event = data as Partial<SubagentLifecyclePayload>;
1833
+ if (typeof event.id !== "string" || !["started", "completed", "failed", "aborted"].includes(String(event.status))) return;
1834
+ if (event.status === "started") {
1835
+ parallelDispatchSucceededThisTurn = true;
1836
+ if (typeof event.parentToolCallId === "string") {
1837
+ const context = parallelTaskContexts.get(event.parentToolCallId);
1838
+ if (context) pendingParallelLifecycleContexts.set(event.id, context);
1839
+ }
1840
+ return;
1841
+ }
1842
+ pendingParallelLifecycle.set(event.id, String(event.status));
1843
+ if (typeof event.parentToolCallId === "string") {
1844
+ const context = parallelTaskContexts.get(event.parentToolCallId);
1845
+ if (context) pendingParallelLifecycleContexts.set(event.id, context);
1846
+ }
1847
+ await reconcileParallelLifecycle(event, pendingParallelLifecycleContexts.get(event.id));
1848
+ scheduleParallelContinuation();
1849
+ });
1850
+
1359
1851
  pi.setLabel("Devflow Project Runtime");
1360
1852
 
1361
1853
  pi.on("session_start", async (_event, ctx) => {
@@ -1411,8 +1903,11 @@ export default function devflowCore(pi: ExtensionAPI) {
1411
1903
  const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
1412
1904
  "consume", "--write", "--json", "--expected-revision", String(rolloverState.revision), "--target-session-id", currentSessionId,
1413
1905
  ]);
1906
+ const synced = consumed.ok
1907
+ ? await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", ["--write", "--no-preserve", "--json"])
1908
+ : undefined;
1414
1909
  const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1415
- if (consumed.ok && after.result.ok && after.snapshot) {
1910
+ if (consumed.ok && synced && controlPlaneSucceeded(synced) && after.result.ok && after.snapshot) {
1416
1911
  if (after.snapshot.action === "blocked") {
1417
1912
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "启动恢复校验失败" });
1418
1913
  ctx.ui.notify(`会话已恢复,但工作流校验失败,未启动 AI:${String(after.snapshot.reason ?? "state validation failed")}`, "error");
@@ -1420,10 +1915,23 @@ export default function devflowCore(pi: ExtensionAPI) {
1420
1915
  }
1421
1916
  setControlState(ctx, executingState("恢复已认领的干净会话"));
1422
1917
  await triggerWorkflow(after.snapshot, "approval");
1423
- } else if (!consumed.ok) {
1918
+ } else if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
1424
1919
  ctx.ui.notify(`已认领的会话交接无法恢复;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
1425
1920
  }
1426
1921
  }
1922
+ if (cliSessionResumeRequested && controlState.phase === "idle" && !pendingRollover) {
1923
+ const report = await runDoctor(ctx);
1924
+ if (!report.ok) {
1925
+ ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
1926
+ return;
1927
+ }
1928
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1929
+ if (!inspected.result.ok || !inspected.snapshot) {
1930
+ ctx.ui.notify(`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1931
+ return;
1932
+ }
1933
+ await resumeInspectedWorkflow(ctx, inspected.snapshot);
1934
+ }
1427
1935
  });
1428
1936
 
1429
1937
  pi.on("session_before_switch", async (event) => {
@@ -1452,13 +1960,16 @@ export default function devflowCore(pi: ExtensionAPI) {
1452
1960
  const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
1453
1961
  "consume", "--write", "--json", "--expected-revision", String(claimedPayload.revision), "--target-session-id", targetSessionId,
1454
1962
  ]);
1455
- if (!consumed.ok) {
1963
+ const synced = consumed.ok
1964
+ ? await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", ["--write", "--no-preserve", "--json"])
1965
+ : undefined;
1966
+ if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
1456
1967
  if (parseJsonResult(consumed)?.reason === "rollover_transition_failed") {
1457
1968
  await enqueueControlledRolloverRequest(ctx.cwd, pending.rolloverId, targetSessionId);
1458
1969
  } else {
1459
1970
  pendingRollover = undefined;
1460
1971
  }
1461
- ctx.ui.notify(`Devflow 会话交接消费失败;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
1972
+ ctx.ui.notify(`Devflow 会话交接消费或 registry 同步失败;工作流保持冻结:${consumed.stderr || consumed.stdout || synced?.stderr || synced?.stdout}`, "error");
1462
1973
  return;
1463
1974
  }
1464
1975
  pendingRollover = undefined;
@@ -1485,9 +1996,34 @@ export default function devflowCore(pi: ExtensionAPI) {
1485
1996
  pi.on("agent_end", async (event, ctx) => {
1486
1997
  if (!activeWorkflowTurn || event.willContinue) return;
1487
1998
  activeWorkflowTurn = false;
1999
+ if (parallelRunActive) {
2000
+ parallelContinuationQueued = false;
2001
+ if (!parallelDispatchSucceededThisTurn && pendingParallelLifecycle.size === 0) {
2002
+ parallelRunActive = false;
2003
+ parallelTaskContexts.clear();
2004
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "并行子 Agent 未启动" });
2005
+ ctx.ui.notify(
2006
+ `并行窗口未启动任何子 Agent,已停止等待不存在的生命周期事件;STATE 未推进。${parallelDispatchFailureThisTurn ? `\n原因:${parallelDispatchFailureThisTurn}` : ""}\n请检查打包 Agent 注册;若当前环境不支持并行,调度器必须按 serial_fallback_order 串行执行。`,
2007
+ "error",
2008
+ );
2009
+ return;
2010
+ }
2011
+ setControlState(ctx, executingState("并行窗口运行中,等待子 Agent 生命周期事件"));
2012
+ // A second detached task may finish while the continuation is running.
2013
+ // Its event is retained and gets a follow-up continuation instead of
2014
+ // being dropped behind the current queue marker.
2015
+ scheduleParallelContinuation();
2016
+ return;
2017
+ }
1488
2018
  if (controlState.phase === "executing") {
1489
2019
  setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
1490
- ctx.ui.notify("当前步骤已结束。请审查输出:明确回复“批准执行”推进,或回复修改意见/“拒绝执行”保持当前步骤。", "info");
2020
+ // UI confirmation may legitimately wait for the user longer than the
2021
+ // host's event-handler deadline. Do not await it from agent_end or the
2022
+ // host reports a timeout and the approval result is lost.
2023
+ void confirmAndAdvanceWorkflowStep(ctx).catch(error => {
2024
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进确认处理失败" });
2025
+ ctx.ui.notify(`推进确认处理失败:${String(error)}`, "error");
2026
+ });
1491
2027
  }
1492
2028
  });
1493
2029
 
@@ -1545,7 +2081,22 @@ export default function devflowCore(pi: ExtensionAPI) {
1545
2081
  }
1546
2082
  return { handled: true };
1547
2083
  }
1548
- if (controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
2084
+ // The in-memory prompt state is intentionally not authoritative. A fresh
2085
+ // session starts idle, so recover the approval arm from the durable Core
2086
+ // snapshot before handing an explicit approval phrase to the model. This
2087
+ // still requires the user's phrase and never auto-advances the workflow.
2088
+ let recoveredApprovalState = false;
2089
+ if (
2090
+ controlAction &&
2091
+ controlState.phase === "idle"
2092
+ ) {
2093
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2094
+ if (inspected.result.ok && inspected.snapshot && isReviewableWorkflowSnapshot(inspected.snapshot)) {
2095
+ setControlState(ctx, waitingUserState("恢复当前步骤审批状态"));
2096
+ recoveredApprovalState = true;
2097
+ }
2098
+ }
2099
+ if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
1549
2100
  if (controlAction === "reject") {
1550
2101
  ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
1551
2102
  return { handled: true };
@@ -1564,6 +2115,17 @@ export default function devflowCore(pi: ExtensionAPI) {
1564
2115
  ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
1565
2116
  return { handled: true };
1566
2117
  }
2118
+ if (checkpoint && stepId === "wireframe-review" && typeof inspected.snapshot.current_feature === "string") {
2119
+ const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb", [
2120
+ "--action", "approve", "--feature", inspected.snapshot.current_feature,
2121
+ "--expected-revision", String(revision), "--all-screens", "--runtime-confirmed", "--write", "--json",
2122
+ ]);
2123
+ if (!controlPlaneSucceeded(approval)) {
2124
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
2125
+ ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
2126
+ return { handled: true };
2127
+ }
2128
+ }
1567
2129
  const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1568
2130
  "--write",
1569
2131
  "--json",
@@ -1645,7 +2207,34 @@ export default function devflowCore(pi: ExtensionAPI) {
1645
2207
 
1646
2208
  pi.on("tool_call", async (event, ctx) => {
1647
2209
  const toolName = String(event.toolName).toLowerCase();
1648
- if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_")) {
2210
+ const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
2211
+ const toolCallId = String(event.toolCallId ?? "");
2212
+ const internalControlResource = internalDevflowControlResource(toolName, event.input);
2213
+ if (isVirtualStateTransitionResource(toolName, event.input)) {
2214
+ return {
2215
+ block: true,
2216
+ reason: "禁止通过 write xd://devflow_transition 推进工作流;步骤产物完成后由 Runtime 弹出用户确认并执行状态迁移。",
2217
+ };
2218
+ }
2219
+ let bootstrapAuthorized = toolName === "devflow_bootstrap" && runtimeBootstrapAuthorizations.has(toolCallId);
2220
+ if (toolName === "devflow_bootstrap" && !bootstrapAuthorized) {
2221
+ const feature = typeof toolInput?.feature === "string" ? toolInput.feature.trim() : "";
2222
+ const phase = typeof toolInput?.phase === "string" && toolInput.phase.trim() ? toolInput.phase.trim() : "默认需求阶段";
2223
+ if (!feature) return { block: true, reason: "bootstrap requires a feature name" };
2224
+ const confirmed = await ctx.ui.confirm(
2225
+ "确认启动新的 Devflow Feature",
2226
+ `Feature:${feature}\n阶段:${phase}\n\n确认后由 Core 原子初始化并切换当前 Feature;不会由 AI 直接修改 STATE.md。若当前仍有未完成 Feature,Core 会拒绝切换并保持原状态。继续吗?`,
2227
+ );
2228
+ if (!confirmed) return { block: true, reason: "User declined runtime bootstrap authorization." };
2229
+ if (!toolCallId) return { block: true, reason: "bootstrap runtime authorization is missing a tool-call binding" };
2230
+ runtimeBootstrapAuthorizations.add(toolCallId);
2231
+ bootstrapAuthorized = true;
2232
+ }
2233
+ if (parallelRunActive && toolName === "task") {
2234
+ const branchContext = parallelBranchContext(event.input);
2235
+ if (branchContext) parallelTaskContexts.set(event.toolCallId, branchContext);
2236
+ }
2237
+ if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
1649
2238
  if (!activeContract.allowedTools.includes(toolName)) {
1650
2239
  return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
1651
2240
  }
@@ -1692,15 +2281,15 @@ export default function devflowCore(pi: ExtensionAPI) {
1692
2281
  } else if (
1693
2282
  controlState.phase === "planning" &&
1694
2283
  toolName !== "devflow_contract" &&
1695
- !isInternalDevflowContractResource(toolName, event.input) &&
2284
+ !internalControlResource &&
1696
2285
  !(toolName === "devflow_evidence" && String((event.input as Record<string, unknown> | undefined)?.action) !== "create") &&
2286
+ !bootstrapAuthorized &&
1697
2287
  !(controlState.intent === "requirement-draft"
1698
2288
  ? isRequirementDraftToolAllowed(toolName, event.input)
1699
2289
  : isPlanningToolAllowed(toolName))
1700
2290
  ) {
1701
2291
  return { block: true, reason: planningToolBlockReason(toolName) };
1702
2292
  }
1703
- const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
1704
2293
  // Registry reconciliation is intentionally the one write operation an
1705
2294
  // agent may request while a step is blocked: it changes only derived
1706
2295
  // artifact metadata and never advances STATE, checkpoints, or ownership.
@@ -1710,7 +2299,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1710
2299
  toolName === "devflow_transition" &&
1711
2300
  toolInput?.action === "complete-step" &&
1712
2301
  toolInput?.confirm === true;
1713
- if (isStateMutationTool(toolName) && !isRegistryReconciliation && !isUserConfirmedStepTransition) {
2302
+ if (isStateMutationTool(toolName) && !isRegistryReconciliation && !isUserConfirmedStepTransition && !bootstrapAuthorized) {
1714
2303
  return {
1715
2304
  block: true,
1716
2305
  reason:
@@ -1729,6 +2318,18 @@ export default function devflowCore(pi: ExtensionAPI) {
1729
2318
  };
1730
2319
  }
1731
2320
  }
2321
+ if (activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
2322
+ const inspected = await inspectWorkflow(ctx.cwd);
2323
+ if (!inspected.result.ok || !inspected.snapshot) {
2324
+ return {
2325
+ block: true,
2326
+ reason: `Active Devflow workflow mutation blocked because authoritative state could not be inspected: ${inspected.result.stderr || inspected.result.stdout}`,
2327
+ };
2328
+ }
2329
+ const fresh = activeContract ? await contractIsFresh(ctx.cwd, activeContract) : false;
2330
+ const reason = stepScopedMutationBlockReason(toolName, event.input, ctx.cwd, inspected.snapshot, activeContract, fresh);
2331
+ if (reason) return { block: true, reason };
2332
+ }
1732
2333
  if (
1733
2334
  !toolName.startsWith("devflow_") &&
1734
2335
  ["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
@@ -1739,10 +2340,30 @@ export default function devflowCore(pi: ExtensionAPI) {
1739
2340
  reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
1740
2341
  };
1741
2342
  }
2343
+ if (
2344
+ !toolName.startsWith("devflow_") &&
2345
+ ["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
2346
+ ) {
2347
+ const scopedFeatures = inputPaths(event.input, ctx.cwd)
2348
+ .map(planningFeatureFromPath)
2349
+ .filter((value): value is string => value !== null);
2350
+ if (scopedFeatures.length > 0) {
2351
+ const state = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
2352
+ const activeFeature = typeof state?.current_feature === "string" ? state.current_feature : undefined;
2353
+ const mismatched = scopedFeatures.find(feature => activeFeature && feature !== activeFeature);
2354
+ if (mismatched) {
2355
+ return {
2356
+ block: true,
2357
+ reason: `feature is not active: requested=${mismatched}, current=${activeFeature}; bootstrap the feature before writing feature-scoped artifacts`,
2358
+ };
2359
+ }
2360
+ }
2361
+ }
1742
2362
  if (toolName.startsWith("devflow_")) return;
1743
2363
  if (
1744
2364
  activeContract &&
1745
2365
  controlState.phase === "executing" &&
2366
+ !internalControlResource &&
1746
2367
  ["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
1747
2368
  ) {
1748
2369
  const paths = inputPaths(event.input, ctx.cwd);
@@ -1804,12 +2425,31 @@ export default function devflowCore(pi: ExtensionAPI) {
1804
2425
  });
1805
2426
 
1806
2427
  pi.on("tool_result", async (event, ctx) => {
2428
+ const taskContext = parallelTaskContexts.get(event.toolCallId);
2429
+ if (parallelRunActive && event.toolName === "task") {
2430
+ if (taskResultHasSuccessfulDispatch(event.details)) {
2431
+ parallelDispatchSucceededThisTurn = true;
2432
+ } else {
2433
+ parallelDispatchFailureThisTurn = event.content
2434
+ .filter(part => part.type === "text")
2435
+ .map(part => part.text)
2436
+ .join("\n")
2437
+ .slice(0, 1000);
2438
+ }
2439
+ // Detached task lifecycle events arrive after tool_result and carry the
2440
+ // parent tool-call id. Keep the correlation until that terminal event.
2441
+ if (taskContext && !parallelRunActive) parallelTaskContexts.delete(event.toolCallId);
2442
+ }
1807
2443
  if (event.isError) return;
1808
2444
  if (activeWorkflowTurn && activeContract) {
1809
- await appendEngineEvidence(ctx.cwd, "engine_dispatched", {
1810
- engine: event.toolName,
1811
- ai_capable: event.toolName === "task" || event.toolName.toLowerCase().includes("ai") || event.toolName.toLowerCase().includes("midscene"),
1812
- });
2445
+ const taskWasDispatched = event.toolName !== "task" || taskResultHasSuccessfulDispatch(event.details);
2446
+ if (taskWasDispatched) {
2447
+ await appendEngineEvidence(ctx.cwd, "engine_dispatched", {
2448
+ engine: event.toolName,
2449
+ ai_capable: event.toolName === "task" || event.toolName.toLowerCase().includes("ai") || event.toolName.toLowerCase().includes("midscene"),
2450
+ ...(taskContext ?? {}),
2451
+ });
2452
+ }
1813
2453
  }
1814
2454
  await runHook(ctx.cwd, "post_tool.py", {
1815
2455
  session_id: sessionId,
@@ -1864,6 +2504,26 @@ export default function devflowCore(pi: ExtensionAPI) {
1864
2504
  },
1865
2505
  });
1866
2506
 
2507
+ pi.registerTool({
2508
+ name: "devflow_wireframe",
2509
+ label: "Devflow Wireframe",
2510
+ description: "Build Core-owned wireframe metadata from the current canonical proposal. Approval is only available through the user-confirmed checkpoint runtime path.",
2511
+ parameters: z.object({
2512
+ action: z.literal("meta"),
2513
+ feature: z.string().min(1),
2514
+ expectedRevision: z.number().int().min(0),
2515
+ }),
2516
+ approval: "write",
2517
+ async execute(_id, params, _signal, _onUpdate, ctx) {
2518
+ return renderResult(
2519
+ await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb", [
2520
+ "--action", "meta", "--feature", params.feature, "--expected-revision", String(params.expectedRevision), "--write", "--json",
2521
+ ]),
2522
+ { action: params.action, feature: params.feature },
2523
+ );
2524
+ },
2525
+ });
2526
+
1867
2527
  pi.registerTool({
1868
2528
 
1869
2529
  name: "devflow_requirement_graph",
@@ -1892,6 +2552,19 @@ export default function devflowCore(pi: ExtensionAPI) {
1892
2552
  }),
1893
2553
  approval: (args) => args.action === "propose" || args.action === "confirm" ? "write" : "read",
1894
2554
  async execute(_id, params, _signal, _onUpdate, ctx) {
2555
+ const graphWrite = params.action === "propose" || params.action === "confirm" || (params.action === "context" && params.write === true);
2556
+ if (graphWrite) {
2557
+ const state = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
2558
+ const currentFeature = typeof state?.current_feature === "string" ? state.current_feature : undefined;
2559
+ if (currentFeature && currentFeature !== params.feature) {
2560
+ return renderResult({
2561
+ ok: false,
2562
+ exitCode: 1,
2563
+ stdout: "",
2564
+ stderr: `feature is not active: requested=${params.feature}, current=${currentFeature}; bootstrap the feature before writing its requirement graph`,
2565
+ });
2566
+ }
2567
+ }
1895
2568
  const args = [params.action, "--feature", params.feature, "--json"];
1896
2569
  if (params.action === "context") {
1897
2570
  if (!params.sliceId) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "context requires sliceId" });
@@ -2391,8 +3064,8 @@ export default function devflowCore(pi: ExtensionAPI) {
2391
3064
  const contract = contractFromRecord(raw as Record<string, unknown>);
2392
3065
  if (!contract) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "Core returned an invalid execution contract." });
2393
3066
  if (input.action === "inspect") return { content: [{ type: "text" as const, text: JSON.stringify(contract, null, 2) }], details: { contract } };
2394
- if (!["planning", "waiting-user", "awaiting-approval"].includes(controlState.phase)) {
2395
- return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from the planning phase or while the step waits for user review." });
3067
+ if (!contractActivationPhaseAllowed(controlState.phase, activeWorkflowTurn)) {
3068
+ return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from planning/review, or for the exact current step during a runtime-triggered workflow turn." });
2396
3069
  }
2397
3070
  const confirmed = await ctx.ui.confirm(
2398
3071
  "激活 Devflow 执行合同",
@@ -2481,6 +3154,9 @@ export default function devflowCore(pi: ExtensionAPI) {
2481
3154
  }),
2482
3155
  approval: "write",
2483
3156
  async execute(_id, params, _signal, _onUpdate, ctx) {
3157
+ // Consume the one-shot runtime authorization before running Core's
3158
+ // atomic bootstrap sequence. A retry must obtain a fresh user approval.
3159
+ runtimeBootstrapAuthorizations.delete(String(_id));
2484
3160
  const phase = params.phase?.trim() || defaultPhaseId(params.feature);
2485
3161
  if (!validPhaseId(phase)) {
2486
3162
  return {
@@ -2523,10 +3199,28 @@ export default function devflowCore(pi: ExtensionAPI) {
2523
3199
  "--json",
2524
3200
  ]);
2525
3201
  if (!controlPlaneSucceeded(artifacts)) return renderResult(artifacts, { stage: "artifacts" });
3202
+ const inspected = await inspectWorkflow(ctx.cwd);
3203
+ if (!inspected.result.ok || !inspected.snapshot) {
3204
+ return renderResult(
3205
+ { ok: false, exitCode: 1, stdout: "", stderr: "bootstrap completed but the resulting workflow state could not be inspected" },
3206
+ { stage: "inspect-after-bootstrap" },
3207
+ );
3208
+ }
3209
+ try {
3210
+ setControlState(ctx, waitingUserState("准备进入需求入口分诊"));
3211
+ await requestControlledRollover(ctx, inspected.snapshot);
3212
+ } catch (error) {
3213
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "会话轮换准备失败" });
3214
+ return renderResult(
3215
+ { ok: false, exitCode: 1, stdout: "", stderr: `bootstrap succeeded but session handoff could not be prepared: ${String(error)}` },
3216
+ { stage: "rollover" },
3217
+ );
3218
+ }
2526
3219
  return renderResult(artifacts, {
2527
3220
  stage: "complete",
2528
3221
  workflow: workflow.stdout,
2529
3222
  state: state.stdout,
3223
+ rollover: "prepared",
2530
3224
  });
2531
3225
  },
2532
3226
  });
@@ -2841,10 +3535,6 @@ export default function devflowCore(pi: ExtensionAPI) {
2841
3535
  );
2842
3536
  return;
2843
3537
  }
2844
- if (inspected.snapshot.action === "workflow_complete") {
2845
- ctx.ui.notify("Devflow workflow is already complete.", "info");
2846
- return;
2847
- }
2848
3538
  // Re-arm a durable rollover left by an interrupted runtime. An
2849
3539
  // explicit resume is the user's authorization for a fresh session to
2850
3540
  // adopt the same project's integrity-checked prepared handoff.
@@ -2864,12 +3554,11 @@ export default function devflowCore(pi: ExtensionAPI) {
2864
3554
  rolloverId: rollover.id,
2865
3555
  };
2866
3556
  await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
3557
+ setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
3558
+ ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
3559
+ return;
2867
3560
  }
2868
- setControlState(ctx, waitingUserState("等待用户审查步骤产物"));
2869
- ctx.ui.notify(
2870
- `Devflow 已恢复到 ${String((compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step))?.id ?? "当前步骤")};请审查当前产物后明确回复“批准执行”,或直接提出修改意见。`,
2871
- "info",
2872
- );
3561
+ await resumeInspectedWorkflow(ctx, inspected.snapshot);
2873
3562
  return;
2874
3563
  }
2875
3564
  if (trimmed.startsWith("start ")) {