@guanlin4924/devflow-pi 0.1.40 → 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 (49) hide show
  1. package/package.json +1 -1
  2. package/plugins/devflow-core/extensions/devflow-core.ts +609 -42
  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 +143 -28
  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 -1
  30. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +3 -3
  31. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +3 -0
  32. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +13 -4
  33. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
  34. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +28 -2
  35. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +139 -14
  36. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph_backfill.rb +143 -0
  37. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +15 -4
  38. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +45 -1
  39. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +10 -1
  40. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +222 -0
  41. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +231 -0
  42. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +457 -0
  43. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +106 -1
  44. package/plugins/devflow-implementation/agents/implementation-worker.md +6 -0
  45. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +107 -37
  46. package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +20 -1
  47. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +1 -1
  48. package/src/constants.ts +1 -1
  49. package/src/runtime.ts +53 -22
@@ -278,14 +278,37 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
278
278
  return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
279
279
  }
280
280
 
281
- /** `xd://devflow_contract` is an in-memory OMP control resource, not a
282
- * project file. The planner must be able to read and activate it while the
283
- * filesystem remains read-only; otherwise initialization deadlocks before it
284
- * can create a bounded execution contract. */
285
- export function isInternalDevflowContractResource(toolName: string, input: unknown): boolean {
286
- 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;
287
288
  const record = input as Record<string, unknown>;
288
- 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";
289
312
  }
290
313
 
291
314
  export type NaturalControlAction = "approve" | "reject";
@@ -301,6 +324,13 @@ export function isStateMutationTool(toolName: string): boolean {
301
324
  return STATE_MUTATION_TOOLS.has(toolName.toLowerCase());
302
325
  }
303
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
+
304
334
  export function planningToolBlockReason(toolName: string): string {
305
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.`;
306
336
  }
@@ -580,6 +610,11 @@ function isCoreOwnedGovernancePath(filePath: string): boolean {
580
610
  || /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath);
581
611
  }
582
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
+
583
618
  function resolveIntelSkillRoot(cwd: string): string | undefined {
584
619
  if (isEmbeddedRuntime()) {
585
620
  const packaged = path.join(resolvePluginRoot(), "devflow-intel", "skills");
@@ -749,6 +784,67 @@ function compactStep(value: unknown): Record<string, unknown> | null {
749
784
  };
750
785
  }
751
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
+
752
848
  function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
753
849
  const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
754
850
  const envelope = {
@@ -778,8 +874,8 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
778
874
  return [
779
875
  "<devflow-runtime-directive>",
780
876
  "Devflow 已通过启动预检。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
781
- "先调用 devflow_state(action=\"inspect\") 重新确认 revision,再由 workflow-orchestrator 路由当前步骤。",
782
- "只按 inspector 返回的当前 step 加载所需 Skill;不得调用 devflow_transition、不得推进下一步骤、不得替用户通过人工 checkpoint。",
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 在用户确认后独占处理。",
783
879
  ...(parallelInstructions.length > 0 ? parallelInstructions : [
784
880
  "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
785
881
  ]),
@@ -846,6 +942,18 @@ function parallelBranchContext(input: unknown): { branch_id: string; attempt: nu
846
942
  return { branch_id: branch, attempt: Number(attempt), branch_revision: Number(revision) };
847
943
  }
848
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
+
849
957
  function isControlPath(filePath: string): boolean {
850
958
  return CONTROL_PATHS.includes(filePath) || CONTROL_PREFIXES.some(prefix => filePath.startsWith(prefix));
851
959
  }
@@ -855,7 +963,7 @@ function isDirectControlMutation(toolName: string, input: unknown, cwd: string):
855
963
  return inputPaths(input, cwd).some(isControlPath);
856
964
  }
857
965
 
858
- function isShellControlMutation(toolName: string, input: unknown): boolean {
966
+ export function isShellControlMutation(toolName: string, input: unknown): boolean {
859
967
  if (!["bash", "shell", "exec_command"].includes(toolName) || !input || typeof input !== "object") {
860
968
  return false;
861
969
  }
@@ -866,7 +974,7 @@ function isShellControlMutation(toolName: string, input: unknown): boolean {
866
974
  command,
867
975
  );
868
976
  const namesControlPath = [...CONTROL_PATHS, ...CONTROL_PREFIXES].some(item => command.includes(item)) || /\.planning\/requirements\//.test(command);
869
- 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(
870
978
  command,
871
979
  );
872
980
  return invokesWriter || (namesControlPath && looksLikeWrite);
@@ -884,8 +992,108 @@ function shellMutationPaths(command: string, cwd: string): { mutating: boolean;
884
992
  return { mutating: true, paths };
885
993
  }
886
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
+
887
1089
  export default function devflowCore(pi: ExtensionAPI) {
888
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;
889
1097
  let sessionId = randomUUID();
890
1098
  let controlState: SessionControlState = initialControlState();
891
1099
  let activeContract: ActiveExecutionContract | undefined;
@@ -1355,10 +1563,17 @@ export default function devflowCore(pi: ExtensionAPI) {
1355
1563
  let activeWorkflowTurn = false;
1356
1564
  let parallelRunActive = false;
1357
1565
  let parallelContinuationQueued = false;
1566
+ let parallelDispatchSucceededThisTurn = false;
1567
+ let parallelDispatchFailureThisTurn = "";
1358
1568
  const pendingParallelLifecycle = new Map<string, string>();
1569
+ const pendingParallelLifecycleContexts = new Map<string, { branch_id: string; attempt: number; branch_revision: number }>();
1359
1570
  const parallelTaskContexts = new Map<string, { branch_id: string; attempt: number; branch_revision: number }>();
1360
1571
  let pendingProjectInitialization: ProjectInitializationRequest | undefined;
1361
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>();
1362
1577
 
1363
1578
  async function enqueueControlledRolloverRequest(cwd: string, rolloverId: string, sourceSessionId: string) {
1364
1579
  const requestPath = path.join(cwd, ".planning", "rollover-request.json");
@@ -1381,6 +1596,12 @@ export default function devflowCore(pi: ExtensionAPI) {
1381
1596
  const rollover = payload.rollover as Record<string, unknown> | undefined;
1382
1597
  const rolloverId = typeof rollover?.id === "string" ? rollover.id : undefined;
1383
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
+ }
1384
1605
  await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
1385
1606
  pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
1386
1607
  }
@@ -1435,16 +1656,169 @@ export default function devflowCore(pi: ExtensionAPI) {
1435
1656
  parallelRunActive = false;
1436
1657
  parallelContinuationQueued = false;
1437
1658
  }
1659
+ if (snapshot.action === "run_parallel_window") {
1660
+ parallelDispatchSucceededThisTurn = false;
1661
+ parallelDispatchFailureThisTurn = "";
1662
+ }
1438
1663
  const preparedSnapshot = await prepareParallelRun(projectRoot ?? process.cwd(), snapshot);
1439
1664
  activeWorkflowTurn = true;
1440
1665
  await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft));
1441
1666
  }
1442
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;
1703
+ }
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
+
1443
1813
  function scheduleParallelContinuation() {
1444
1814
  if (parallelContinuationQueued || pendingParallelLifecycle.size === 0) return;
1445
1815
  parallelContinuationQueued = true;
1446
- const events = [...pendingParallelLifecycle.entries()].map(([id, status]) => `${id}:${status}`);
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
+ });
1447
1820
  pendingParallelLifecycle.clear();
1821
+ pendingParallelLifecycleContexts.clear();
1448
1822
  pi.sendMessage({
1449
1823
  customType: "devflow-parallel-lifecycle",
1450
1824
  content: `并行子 Agent 生命周期事件已到达(${events.join(", ")})。请重新 inspect 整个 parallel run,逐一处理所有已完成/失败分支,必要时调用 Core 的 branch complete/fail/retry 动作,然后继续派发仍满足前置依赖的分支;不得直接 join。`,
@@ -1453,12 +1827,24 @@ export default function devflowCore(pi: ExtensionAPI) {
1453
1827
  }, { triggerTurn: true, deliverAs: "nextTurn" });
1454
1828
  }
1455
1829
 
1456
- pi.events.on(TASK_SUBAGENT_LIFECYCLE_CHANNEL, data => {
1830
+ pi.events.on(TASK_SUBAGENT_LIFECYCLE_CHANNEL, async data => {
1457
1831
  if (!parallelRunActive || !data || typeof data !== "object") return;
1458
1832
  const event = data as Partial<SubagentLifecyclePayload>;
1459
1833
  if (typeof event.id !== "string" || !["started", "completed", "failed", "aborted"].includes(String(event.status))) return;
1460
- if (event.status === "started") 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
+ }
1461
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));
1462
1848
  scheduleParallelContinuation();
1463
1849
  });
1464
1850
 
@@ -1517,8 +1903,11 @@ export default function devflowCore(pi: ExtensionAPI) {
1517
1903
  const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
1518
1904
  "consume", "--write", "--json", "--expected-revision", String(rolloverState.revision), "--target-session-id", currentSessionId,
1519
1905
  ]);
1906
+ const synced = consumed.ok
1907
+ ? await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", ["--write", "--no-preserve", "--json"])
1908
+ : undefined;
1520
1909
  const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1521
- if (consumed.ok && after.result.ok && after.snapshot) {
1910
+ if (consumed.ok && synced && controlPlaneSucceeded(synced) && after.result.ok && after.snapshot) {
1522
1911
  if (after.snapshot.action === "blocked") {
1523
1912
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "启动恢复校验失败" });
1524
1913
  ctx.ui.notify(`会话已恢复,但工作流校验失败,未启动 AI:${String(after.snapshot.reason ?? "state validation failed")}`, "error");
@@ -1526,10 +1915,23 @@ export default function devflowCore(pi: ExtensionAPI) {
1526
1915
  }
1527
1916
  setControlState(ctx, executingState("恢复已认领的干净会话"));
1528
1917
  await triggerWorkflow(after.snapshot, "approval");
1529
- } else if (!consumed.ok) {
1918
+ } else if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
1530
1919
  ctx.ui.notify(`已认领的会话交接无法恢复;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
1531
1920
  }
1532
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
+ }
1533
1935
  });
1534
1936
 
1535
1937
  pi.on("session_before_switch", async (event) => {
@@ -1558,13 +1960,16 @@ export default function devflowCore(pi: ExtensionAPI) {
1558
1960
  const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
1559
1961
  "consume", "--write", "--json", "--expected-revision", String(claimedPayload.revision), "--target-session-id", targetSessionId,
1560
1962
  ]);
1561
- 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)) {
1562
1967
  if (parseJsonResult(consumed)?.reason === "rollover_transition_failed") {
1563
1968
  await enqueueControlledRolloverRequest(ctx.cwd, pending.rolloverId, targetSessionId);
1564
1969
  } else {
1565
1970
  pendingRollover = undefined;
1566
1971
  }
1567
- ctx.ui.notify(`Devflow 会话交接消费失败;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
1972
+ ctx.ui.notify(`Devflow 会话交接消费或 registry 同步失败;工作流保持冻结:${consumed.stderr || consumed.stdout || synced?.stderr || synced?.stdout}`, "error");
1568
1973
  return;
1569
1974
  }
1570
1975
  pendingRollover = undefined;
@@ -1593,6 +1998,16 @@ export default function devflowCore(pi: ExtensionAPI) {
1593
1998
  activeWorkflowTurn = false;
1594
1999
  if (parallelRunActive) {
1595
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
+ }
1596
2011
  setControlState(ctx, executingState("并行窗口运行中,等待子 Agent 生命周期事件"));
1597
2012
  // A second detached task may finish while the continuation is running.
1598
2013
  // Its event is retained and gets a follow-up continuation instead of
@@ -1602,7 +2017,13 @@ export default function devflowCore(pi: ExtensionAPI) {
1602
2017
  }
1603
2018
  if (controlState.phase === "executing") {
1604
2019
  setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
1605
- 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
+ });
1606
2027
  }
1607
2028
  });
1608
2029
 
@@ -1660,7 +2081,22 @@ export default function devflowCore(pi: ExtensionAPI) {
1660
2081
  }
1661
2082
  return { handled: true };
1662
2083
  }
1663
- 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") {
1664
2100
  if (controlAction === "reject") {
1665
2101
  ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
1666
2102
  return { handled: true };
@@ -1679,6 +2115,17 @@ export default function devflowCore(pi: ExtensionAPI) {
1679
2115
  ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
1680
2116
  return { handled: true };
1681
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
+ }
1682
2129
  const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1683
2130
  "--write",
1684
2131
  "--json",
@@ -1760,11 +2207,34 @@ export default function devflowCore(pi: ExtensionAPI) {
1760
2207
 
1761
2208
  pi.on("tool_call", async (event, ctx) => {
1762
2209
  const toolName = String(event.toolName).toLowerCase();
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
+ }
1763
2233
  if (parallelRunActive && toolName === "task") {
1764
2234
  const branchContext = parallelBranchContext(event.input);
1765
2235
  if (branchContext) parallelTaskContexts.set(event.toolCallId, branchContext);
1766
2236
  }
1767
- if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_")) {
2237
+ if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
1768
2238
  if (!activeContract.allowedTools.includes(toolName)) {
1769
2239
  return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
1770
2240
  }
@@ -1811,15 +2281,15 @@ export default function devflowCore(pi: ExtensionAPI) {
1811
2281
  } else if (
1812
2282
  controlState.phase === "planning" &&
1813
2283
  toolName !== "devflow_contract" &&
1814
- !isInternalDevflowContractResource(toolName, event.input) &&
2284
+ !internalControlResource &&
1815
2285
  !(toolName === "devflow_evidence" && String((event.input as Record<string, unknown> | undefined)?.action) !== "create") &&
2286
+ !bootstrapAuthorized &&
1816
2287
  !(controlState.intent === "requirement-draft"
1817
2288
  ? isRequirementDraftToolAllowed(toolName, event.input)
1818
2289
  : isPlanningToolAllowed(toolName))
1819
2290
  ) {
1820
2291
  return { block: true, reason: planningToolBlockReason(toolName) };
1821
2292
  }
1822
- const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
1823
2293
  // Registry reconciliation is intentionally the one write operation an
1824
2294
  // agent may request while a step is blocked: it changes only derived
1825
2295
  // artifact metadata and never advances STATE, checkpoints, or ownership.
@@ -1829,7 +2299,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1829
2299
  toolName === "devflow_transition" &&
1830
2300
  toolInput?.action === "complete-step" &&
1831
2301
  toolInput?.confirm === true;
1832
- if (isStateMutationTool(toolName) && !isRegistryReconciliation && !isUserConfirmedStepTransition) {
2302
+ if (isStateMutationTool(toolName) && !isRegistryReconciliation && !isUserConfirmedStepTransition && !bootstrapAuthorized) {
1833
2303
  return {
1834
2304
  block: true,
1835
2305
  reason:
@@ -1848,6 +2318,18 @@ export default function devflowCore(pi: ExtensionAPI) {
1848
2318
  };
1849
2319
  }
1850
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
+ }
1851
2333
  if (
1852
2334
  !toolName.startsWith("devflow_") &&
1853
2335
  ["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
@@ -1858,10 +2340,30 @@ export default function devflowCore(pi: ExtensionAPI) {
1858
2340
  reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
1859
2341
  };
1860
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
+ }
1861
2362
  if (toolName.startsWith("devflow_")) return;
1862
2363
  if (
1863
2364
  activeContract &&
1864
2365
  controlState.phase === "executing" &&
2366
+ !internalControlResource &&
1865
2367
  ["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
1866
2368
  ) {
1867
2369
  const paths = inputPaths(event.input, ctx.cwd);
@@ -1923,15 +2425,31 @@ export default function devflowCore(pi: ExtensionAPI) {
1923
2425
  });
1924
2426
 
1925
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
+ }
1926
2443
  if (event.isError) return;
1927
2444
  if (activeWorkflowTurn && activeContract) {
1928
- const taskContext = parallelTaskContexts.get(event.toolCallId);
1929
- await appendEngineEvidence(ctx.cwd, "engine_dispatched", {
1930
- engine: event.toolName,
1931
- ai_capable: event.toolName === "task" || event.toolName.toLowerCase().includes("ai") || event.toolName.toLowerCase().includes("midscene"),
1932
- ...(taskContext ?? {}),
1933
- });
1934
- if (taskContext) parallelTaskContexts.delete(event.toolCallId);
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
+ }
1935
2453
  }
1936
2454
  await runHook(ctx.cwd, "post_tool.py", {
1937
2455
  session_id: sessionId,
@@ -1986,6 +2504,26 @@ export default function devflowCore(pi: ExtensionAPI) {
1986
2504
  },
1987
2505
  });
1988
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
+
1989
2527
  pi.registerTool({
1990
2528
 
1991
2529
  name: "devflow_requirement_graph",
@@ -2014,6 +2552,19 @@ export default function devflowCore(pi: ExtensionAPI) {
2014
2552
  }),
2015
2553
  approval: (args) => args.action === "propose" || args.action === "confirm" ? "write" : "read",
2016
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
+ }
2017
2568
  const args = [params.action, "--feature", params.feature, "--json"];
2018
2569
  if (params.action === "context") {
2019
2570
  if (!params.sliceId) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "context requires sliceId" });
@@ -2513,8 +3064,8 @@ export default function devflowCore(pi: ExtensionAPI) {
2513
3064
  const contract = contractFromRecord(raw as Record<string, unknown>);
2514
3065
  if (!contract) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "Core returned an invalid execution contract." });
2515
3066
  if (input.action === "inspect") return { content: [{ type: "text" as const, text: JSON.stringify(contract, null, 2) }], details: { contract } };
2516
- if (!["planning", "waiting-user", "awaiting-approval"].includes(controlState.phase)) {
2517
- 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." });
2518
3069
  }
2519
3070
  const confirmed = await ctx.ui.confirm(
2520
3071
  "激活 Devflow 执行合同",
@@ -2603,6 +3154,9 @@ export default function devflowCore(pi: ExtensionAPI) {
2603
3154
  }),
2604
3155
  approval: "write",
2605
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));
2606
3160
  const phase = params.phase?.trim() || defaultPhaseId(params.feature);
2607
3161
  if (!validPhaseId(phase)) {
2608
3162
  return {
@@ -2645,10 +3199,28 @@ export default function devflowCore(pi: ExtensionAPI) {
2645
3199
  "--json",
2646
3200
  ]);
2647
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
+ }
2648
3219
  return renderResult(artifacts, {
2649
3220
  stage: "complete",
2650
3221
  workflow: workflow.stdout,
2651
3222
  state: state.stdout,
3223
+ rollover: "prepared",
2652
3224
  });
2653
3225
  },
2654
3226
  });
@@ -2963,10 +3535,6 @@ export default function devflowCore(pi: ExtensionAPI) {
2963
3535
  );
2964
3536
  return;
2965
3537
  }
2966
- if (inspected.snapshot.action === "workflow_complete") {
2967
- ctx.ui.notify("Devflow workflow is already complete.", "info");
2968
- return;
2969
- }
2970
3538
  // Re-arm a durable rollover left by an interrupted runtime. An
2971
3539
  // explicit resume is the user's authorization for a fresh session to
2972
3540
  // adopt the same project's integrity-checked prepared handoff.
@@ -2986,12 +3554,11 @@ export default function devflowCore(pi: ExtensionAPI) {
2986
3554
  rolloverId: rollover.id,
2987
3555
  };
2988
3556
  await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
3557
+ setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
3558
+ ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
3559
+ return;
2989
3560
  }
2990
- setControlState(ctx, waitingUserState("等待用户审查步骤产物"));
2991
- ctx.ui.notify(
2992
- `Devflow 已恢复到 ${String((compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step))?.id ?? "当前步骤")};请审查当前产物后明确回复“批准执行”,或直接提出修改意见。`,
2993
- "info",
2994
- );
3561
+ await resumeInspectedWorkflow(ctx, inspected.snapshot);
2995
3562
  return;
2996
3563
  }
2997
3564
  if (trimmed.startsWith("start ")) {