@guanlin4924/devflow-pi 0.1.105 → 0.1.107

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 (66) hide show
  1. package/README.md +10 -8
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +287 -394
  4. package/plugins/devflow-core/runtime/hooks/context_sync_gate.py +3 -1
  5. package/plugins/devflow-core/runtime/hooks/hook_common.py +6 -5
  6. package/plugins/devflow-core/runtime/hooks/index_gate.py +19 -26
  7. package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +2 -0
  8. package/plugins/devflow-core/skills/wireframe-render/SKILL.md +2 -0
  9. package/plugins/devflow-core/skills/wireframe-review/SKILL.md +2 -0
  10. package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +2 -0
  11. package/plugins/devflow-core/skills/workflow-dynamic/SKILL.md +41 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +37 -50
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +46 -191
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/artifact_specs.rb +12 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dynamic_plan.rb +504 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +38 -1
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_policy.rb +11 -0
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_result.rb +12 -0
  19. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +4 -9
  20. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +19 -86
  21. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +24 -13
  22. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +4 -4
  23. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +7 -1
  24. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +17 -39
  25. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +39 -3
  26. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +2 -3
  27. package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +0 -1
  28. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +13 -5
  29. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +1 -1
  30. package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +2 -0
  31. package/plugins/devflow-design/skills/workflow-api-call-guide/SKILL.md +2 -0
  32. package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +3 -1
  33. package/plugins/devflow-design/skills/workflow-api-supplement/SKILL.md +2 -0
  34. package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +2 -0
  35. package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +6 -4
  36. package/plugins/devflow-design/skills/workflow-design-rule-pack/SKILL.md +2 -0
  37. package/plugins/devflow-design/skills/workflow-feature-breakdown/SKILL.md +2 -0
  38. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +6 -4
  39. package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +3 -1
  40. package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +4 -2
  41. package/plugins/devflow-implementation/agents/implementation-worker.md +2 -8
  42. package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +5 -0
  43. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +3 -1
  44. package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +6 -1
  45. package/plugins/devflow-intel/skills/code-index-query/SKILL.md +2 -0
  46. package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +2 -0
  47. package/plugins/devflow-intel/skills/workflow-context-sync/SKILL.md +2 -0
  48. package/plugins/devflow-intel/skills/workflow-update-navigation/SKILL.md +3 -1
  49. package/plugins/devflow-quality/agents/code-reviewer.md +2 -0
  50. package/plugins/devflow-quality/skills/code-review/SKILL.md +7 -2
  51. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -1
  52. package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +2 -0
  53. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +2 -0
  54. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +2 -0
  55. package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +2 -0
  56. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +7 -5
  57. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +11 -6
  58. package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +2 -0
  59. package/plugins/devflow-requirements/skills/workflow-demand-clarify/SKILL.md +2 -0
  60. package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +2 -0
  61. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +6 -2
  62. package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +5 -0
  63. package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +2 -0
  64. package/src/constants.ts +1 -1
  65. package/src/dynamic-workflow.ts +31 -0
  66. package/templates/omp/AGENTS.md +8 -3
@@ -19,6 +19,7 @@ import { generateProjectDocuments } from "../../../src/project-documents.ts";
19
19
  import { preparedRolloverRevision } from "../../../src/session-rollover.ts";
20
20
  import { describeStartupIssue, renderStartupBriefing } from "../../../src/startup-briefing.ts";
21
21
  import { PRODUCT_VERSION } from "../../../src/constants.ts";
22
+ import { dynamicWorkflowDirective, renderDynamicWorkflow } from "../../../src/dynamic-workflow.ts";
22
23
  import {
23
24
  FIX_LABELS,
24
25
  buildHelpBundle,
@@ -233,12 +234,13 @@ type ActiveExecutionContract = {
233
234
  stateRevision?: number;
234
235
  graphRevision?: number;
235
236
  workflowFingerprint?: string;
237
+ scope?: string;
238
+ branchContracts?: unknown[];
236
239
  allowedPaths: string[];
237
240
  allowedAgents: string[];
238
241
  allowedTools: string[];
239
242
  checkerIds: string[];
240
243
  toolBudget: number;
241
- hardGateMode: "chain" | "core";
242
244
  frozenHash: string;
243
245
  contractPath?: string;
244
246
  acceptance: string[];
@@ -256,12 +258,13 @@ function contractFromRecord(record: Record<string, unknown>): ActiveExecutionCon
256
258
  stateRevision: typeof record.state_revision === "number" ? record.state_revision : undefined,
257
259
  graphRevision: typeof record.graph_revision === "number" ? record.graph_revision : undefined,
258
260
  workflowFingerprint: typeof record.workflow_fingerprint === "string" ? record.workflow_fingerprint : undefined,
261
+ scope: typeof record.scope === "string" ? record.scope : undefined,
262
+ branchContracts: Array.isArray(record.branch_contracts) ? record.branch_contracts : undefined,
259
263
  allowedPaths: strings("allowed_paths"),
260
264
  allowedAgents: strings("allowed_agents"),
261
265
  allowedTools: strings("allowed_tools"),
262
266
  checkerIds: strings("checker_ids"),
263
267
  toolBudget: typeof record.tool_budget === "number" ? record.tool_budget : 0,
264
- hardGateMode: record.hard_gate_mode === "core" ? "core" : "chain",
265
268
  frozenHash: String(record.frozen_hash ?? ""),
266
269
  acceptance: strings("acceptance"),
267
270
  allowedGraphDelta: strings("allowed_graph_delta"),
@@ -271,10 +274,6 @@ function contractFromRecord(record: Record<string, unknown>): ActiveExecutionCon
271
274
  return contract;
272
275
  }
273
276
 
274
- function coreHardGatesEnabled(contract: ActiveExecutionContract | undefined): boolean {
275
- return contract?.hardGateMode === "core";
276
- }
277
-
278
277
  const READ_ONLY_TOOLS = new Set([
279
278
  "read",
280
279
  "grep",
@@ -821,6 +820,7 @@ const REQUIRED_EXTENSION_TOOLS = [
821
820
  ];
822
821
 
823
822
  const REQUIRED_SKILLS = [
823
+ "workflow-dynamic",
824
824
  "code-index-query",
825
825
  "code-review",
826
826
  "requirement-feasibility-review",
@@ -878,6 +878,7 @@ const REQUIRED_MODEL_ROLES = ["@smol", "@task", "@slow"];
878
878
  const REQUIRED_AGENT_NAMES = new Set(Object.keys(REQUIRED_AGENTS));
879
879
 
880
880
  const REQUIRED_SCRIPTS = [
881
+ "workflow-orchestrator/scripts/dynamic_plan.rb",
881
882
  "workflow-orchestrator/scripts/init_workflow.rb",
882
883
  "workflow-orchestrator/scripts/init_state.rb",
883
884
  "workflow-orchestrator/scripts/phase_ownership.rb",
@@ -1316,7 +1317,7 @@ function selectedWorkflowStep(snapshot: Record<string, unknown>): Record<string,
1316
1317
  * completed step. This predicate lets the runtime re-arm approval handling
1317
1318
  * from the Core-owned snapshot without granting approval by itself.
1318
1319
  */
1319
- function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>, options: { strictRegistry?: boolean } = {}): boolean {
1320
+ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>, options: { strictRegistry?: boolean; allowAdvisory?: boolean } = {}): boolean {
1320
1321
  const status = snapshot.status;
1321
1322
  if (status !== "waiting_checkpoint" && status !== "not_started" && status !== "in_progress") return false;
1322
1323
  const step = snapshot.current_step;
@@ -1326,7 +1327,8 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>, options
1326
1327
  // Core-generated quality findings are advisory by default. Re-arm the AI
1327
1328
  // review even when an output is stale or its generated validity metadata is
1328
1329
  // incomplete; only the control-plane shape itself remains a hard boundary.
1329
- if (process.env.DEVFLOW_SOFT_GATES !== "0" && snapshot.action === "await_user_confirmation") return true;
1330
+ if (snapshot.action !== "run_parallel_window" &&
1331
+ (options.allowAdvisory || snapshot.action === "await_user_confirmation")) return true;
1330
1332
  const outputs = Array.isArray(stepRecord.outputs) ? stepRecord.outputs : [];
1331
1333
  const requiredOutputs = outputs.filter(output => {
1332
1334
  if (!output || typeof output !== "object") return false;
@@ -1386,7 +1388,7 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
1386
1388
  return "blocked";
1387
1389
  }
1388
1390
 
1389
- function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string): string {
1391
+ export function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string): string {
1390
1392
  const selectedStep = selectedWorkflowStep(snapshot);
1391
1393
  const reviewInstructions = selectedStep?.id === "code-review"
1392
1394
  ? [
@@ -1407,6 +1409,7 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
1407
1409
  checkpoint: snapshot.checkpoint,
1408
1410
  step: selectedStep,
1409
1411
  parallel_window: snapshot.parallel_window,
1412
+ parallel_run: snapshot.parallel_run,
1410
1413
  failure_branch: snapshot.failure_branch,
1411
1414
  reason: snapshot.reason,
1412
1415
  recovery: snapshot.recovery,
@@ -1414,7 +1417,8 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
1414
1417
  };
1415
1418
  const parallelInstructions = snapshot.action === "run_parallel_window"
1416
1419
  ? [
1417
- "这是并行窗口:先按 parallel_run 中的 branch 记录逐个调用 task;一个 task 只能负责一个 workflow_step,不得把多个 step 合并到一个子 Agent。",
1420
+ "当前合同 scope=parallel_window:current_step 只是汇合锚点,不要执行 integration-verify 或读取它的 Skill;按 parallel_window.branch_steps 中当前可运行的分支读取各自精确 skill,并遵守合同 branch_contracts 的逐分支权限。",
1421
+ "这是并行窗口:仅派发 parallel_window.branch_steps 中当前可运行的分支,并从 parallel_run 中匹配 branch 记录逐个调用 task;一个 task 只能负责一个 workflow_step,不得把多个 step 合并到一个子 Agent。",
1418
1422
  "本窗口的 branch step(backend-tech-spec、api-call-guide、ui-spec、feature-breakdown、frontend-tech-spec、api-supplement)统一使用白名单 agent implementation-worker;不要把 workflow step 名称当作 agent 名称。",
1419
1423
  "只有执行验证类步骤才使用 verification-runner;project-scout、requirements-auditor、contract-auditor、devflow-code-reviewer 不属于本并行窗口的默认执行 agent。",
1420
1424
  "每个 task 的提示必须原样包含 DEVFLOW_BRANCH_ID、DEVFLOW_WORKFLOW_STEP、DEVFLOW_TRACK、DEVFLOW_ATTEMPT 和 DEVFLOW_BRANCH_REVISION,并只写该分支的白名单产物。",
@@ -1424,16 +1428,15 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
1424
1428
  : [];
1425
1429
  return [
1426
1430
  "<devflow-runtime-directive>",
1427
- briefing
1428
- ? "Devflow Runtime 已完成开工预检(环境、交接、状态、合同、输入),以下开工简报中的事实已经核实;执行合同已激活,不要再次调用 devflow_state inspect 或 devflow_contract activate。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。"
1429
- : "Devflow Runtime 已完成环境与状态检查。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
1431
+ ...(["当前为全软门禁模式:产物、索引、合同、质量、审查和恢复检查只提供诊断。缺失输入先利用已有证据尽力完成,未解决项写入报告;不得因检查未通过而停止整个流程,不得伪造测试通过或人工批准。CP 节点不强制审批,用户要求下一步即可继续。真实工具执行失败仍须如实报告。"]),
1432
+ "读取 skill://workflow-pipeline,按当前步骤或并行分支执行;开工简报、执行合同与 Skill 中的门禁要求在本模式下均为建议。",
1430
1433
  ...(briefing
1431
1434
  ? ["<startup-briefing>", briefing, "</startup-briefing>"]
1432
- : ["先调用 devflow_state(action=\"inspect\") 重新确认 revision,再调用 devflow_contract(action=\"status\");若没有匹配当前 feature/current_step 的 fresh contract,立即调用 devflow_contract(action=\"activate\", feature=<current_feature>, step=<current_step>),激活成功后才能执行或写入。"]),
1433
- "只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
1434
- ...reviewInstructions,
1435
+ : ["可按需查看状态与合同;缺失或过期合同不阻止执行。"]),
1436
+ "串行步骤按 inspector.current_step,并行窗口按可运行分支加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
1437
+ ...([]),
1435
1438
  ...(parallelInstructions.length > 0 ? parallelInstructions : [
1436
- "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
1439
+ "完成当前步骤后报告证据、未决项和检查警告。用户要求下一步即可继续;不要把质量检查失败当作流程阻塞,也不要自报未执行的检查已通过。",
1437
1440
  ]),
1438
1441
  "当前步骤信封:",
1439
1442
  JSON.stringify(envelope, null, 2),
@@ -1535,6 +1538,7 @@ export function isShellControlMutation(toolName: string, input: unknown): boolea
1535
1538
  }
1536
1539
  const command = String((input as Record<string, unknown>).command ?? (input as Record<string, unknown>).cmd ?? "");
1537
1540
  if (!command) return false;
1541
+ if (/\bdynamic_plan\.rb\b/.test(command)) return true;
1538
1542
  const invokesWriter =
1539
1543
  /workflow-orchestrator\/scripts\/(?:advance_state|init_state|init_workflow|sync_artifacts|requirement_graph|machine_evidence|execution_contract)\.rb[^\n]*--write/.test(
1540
1544
  command,
@@ -1997,6 +2001,10 @@ export default function devflowCore(pi: ExtensionAPI) {
1997
2001
  const reviewBindingKey = randomBytes(32).toString("hex");
1998
2002
  const pendingReviewTasks = new Map<string, { feature: string; contractId: string }>();
1999
2003
  const completedReviewTasks = new Map<string, { feature: string; contractId: string; report: Record<string, unknown> }>();
2004
+ const dynamicReviewBindings = new Map<string, { taskId: string; attempt: string; revision: number }>();
2005
+ const dynamicReviewResults = new Map<string, { taskId: string; attempt: string; revision: number; report: Record<string, unknown> }>();
2006
+ let dynamicTurnRevision: number | undefined;
2007
+ let dynamicTurnProgress: string | undefined;
2000
2008
  let latestReviewTaskCallId: string | undefined;
2001
2009
  let projectRoot: string | undefined;
2002
2010
 
@@ -2109,6 +2117,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2109
2117
  const content = lines.map((line, index) => {
2110
2118
  if (index === 0) return theme.bold(theme.fg("accent", line));
2111
2119
  if (line.startsWith("──")) return theme.fg("accent", line);
2120
+ if (line.startsWith("—")) return theme.fg("dim", line);
2112
2121
  return theme.fg("muted", line);
2113
2122
  }).join("\n");
2114
2123
  panel.addChild(new Text(content, 0, 0));
@@ -2160,7 +2169,12 @@ export default function devflowCore(pi: ExtensionAPI) {
2160
2169
  graph && typeof graph === "object" && typeof (graph as Record<string, unknown>).revision === "number"
2161
2170
  ? (graph as Record<string, number>).revision
2162
2171
  : undefined;
2163
- return stateRevision === contract.stateRevision && graphRevision === contract.graphRevision;
2172
+ if (stateRevision !== contract.stateRevision || graphRevision !== contract.graphRevision) return false;
2173
+ const inspected = parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/execution_contract.rb", [
2174
+ "inspect", "--feature", contract.feature, "--step", contract.step, "--json",
2175
+ ]));
2176
+ const current = inspected?.contract as Record<string, unknown> | undefined;
2177
+ return current?.frozen_hash === contract.frozenHash;
2164
2178
  }
2165
2179
 
2166
2180
  // A successful graph mutation is already constrained by the active contract's
@@ -2254,22 +2268,6 @@ export default function devflowCore(pi: ExtensionAPI) {
2254
2268
  }
2255
2269
  const contract = contractFromRecord(raw as Record<string, unknown>);
2256
2270
  if (!contract) return { ok: false, reason: "Core returned an invalid execution contract." };
2257
- const confirmed = await ctx.ui.confirm(
2258
- "激活 Devflow 执行合同",
2259
- [
2260
- `合同:${contract.contractId}`,
2261
- `Feature:${contract.feature}`,
2262
- `步骤:${contract.step}`,
2263
- `STATE revision:${contract.stateRevision ?? "未初始化"}`,
2264
- `图谱 revision:${contract.graphRevision ?? 0}`,
2265
- `可写路径:${contract.allowedPaths.join(", ")}`,
2266
- `允许 Agent:${contract.allowedAgents.join(", ")}`,
2267
- `固定检查器:${contract.checkerIds.join(", ") || "无"}`,
2268
- `冻结哈希:${contract.frozenHash}`,
2269
- "确认后低成本模型只能在以上范围执行;未知项必须阻塞升级。",
2270
- ].join("\n"),
2271
- );
2272
- if (!confirmed) return { ok: false, reason: "User declined execution contract activation." };
2273
2271
  try {
2274
2272
  const createArgs = ["create", "--feature", feature, "--step", step, "--write", "--json"];
2275
2273
  if (contract.stateRevision !== undefined) createArgs.push("--expected-state-revision", String(contract.stateRevision));
@@ -2618,80 +2616,17 @@ export default function devflowCore(pi: ExtensionAPI) {
2618
2616
  snapshot: Record<string, unknown>,
2619
2617
  options: { doctorAlreadyPassed?: boolean; handoffPath?: string } = {},
2620
2618
  ): Promise<{ ok: true; briefing?: string } | { ok: false }> {
2621
- if (options.doctorAlreadyPassed !== true) {
2622
- const report = await runDoctor(ctx);
2623
- if (!report.ok) {
2624
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检失败" });
2625
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
2626
- return { ok: false };
2627
- }
2628
- }
2629
- if (workflowResumeDisposition(snapshot) !== "execute" || snapshot.action === "run_parallel_window") {
2630
- return { ok: true };
2631
- }
2632
-
2633
- try {
2634
- const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
2635
- const step = String(selectedWorkflowStep(snapshot)?.id ?? "");
2636
- if (!feature || !step) {
2637
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
2638
- ctx.ui.notify("开工预检未通过:当前 Feature 或步骤不可用。", "error");
2639
- return { ok: false };
2640
- }
2641
- let contract = activeContract;
2642
- if (!contract || contract.feature !== feature || contract.step !== step || !(await contractIsFresh(ctx.cwd, contract))) {
2643
- const activated = await activateExecutionContract(ctx, feature, step);
2644
- if (!activated.ok) {
2645
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
2646
- ctx.ui.notify(`开工预检:执行合同未激活。${activated.reason}`, "error");
2647
- return { ok: false };
2648
- }
2649
- contract = activated.contract;
2650
- }
2651
- if (!contract.contractPath) {
2652
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
2653
- ctx.ui.notify("开工预检:执行合同未激活。合同冻结路径不可用。", "error");
2654
- return { ok: false };
2655
- }
2656
- const contractAbsolute = path.resolve(ctx.cwd, contract.contractPath);
2657
- const contractRelative = path.relative(ctx.cwd, contractAbsolute).split(path.sep).join("/");
2658
- if (!contractRelative || contractRelative === ".." || contractRelative.startsWith("../")) {
2659
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
2660
- ctx.ui.notify("开工预检:执行合同未激活。合同冻结路径越出项目。", "error");
2661
- return { ok: false };
2662
- }
2663
- const preflight = await runPython(ctx.cwd, "workflow-orchestrator/scripts/startup_preflight.py", [
2664
- "--root", ctx.cwd,
2665
- "--feature", feature,
2666
- "--step", step,
2667
- "--contract", contractRelative,
2668
- ...(options.handoffPath ? ["--handoff", options.handoffPath] : []),
2669
- ]);
2670
- const payload = parseJsonOutput(preflight);
2671
- if (!payload || payload.status !== "ready") {
2672
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
2619
+ const feature = String(snapshot.current_feature ?? "");
2620
+ const step = String(selectedWorkflowStep(snapshot)?.id ?? "");
2621
+ const diagnostics = await runPython(ctx.cwd, "workflow-orchestrator/scripts/startup_preflight.py", [
2622
+ "--root", ctx.cwd, "--feature", feature, "--step", step,
2623
+ ...(options.handoffPath ? ["--handoff", options.handoffPath] : []),
2624
+ ]);
2625
+ const payload = parseJsonOutput(diagnostics);
2673
2626
  const issues = Array.isArray(payload?.issues) ? payload.issues : [];
2674
- const details = issues.length > 0
2675
- ? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
2676
- : "- 开工预检输出无法解析";
2677
- ctx.ui.notify(`开工预检未通过:\n${details}\n可以说「下一步」,由 Devflow 诊断并给出处理办法。`, "error");
2678
- return { ok: false };
2627
+ if (!diagnostics.ok || issues.length) ctx.ui.notify(`软门禁提示:${JSON.stringify(issues.length ? issues : diagnostics.stderr)}`, "info");
2628
+ return { ok: true, briefing: `软门禁模式:以下是诊断结果,缺项不阻止执行,失败检查不代表通过。\n${JSON.stringify(payload ?? { error: diagnostics.stderr })}` };
2679
2629
  }
2680
- const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
2681
- const handoff = payload.handoff && typeof payload.handoff === "object" ? payload.handoff as Record<string, unknown> : {};
2682
- return {
2683
- ok: true,
2684
- briefing: renderStartupBriefing(facts, handoff, {
2685
- revision: snapshot.revision,
2686
- handoffProvided: Boolean(options.handoffPath),
2687
- }),
2688
- };
2689
- } catch (error) {
2690
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
2691
- ctx.ui.notify(`开工预检未通过:${error instanceof Error ? error.message : String(error)}`, "error");
2692
- return { ok: false };
2693
- }
2694
- }
2695
2630
 
2696
2631
  async function startWorkflowAfterPreflight(
2697
2632
  ctx: DoctorContext & {
@@ -2708,6 +2643,21 @@ export default function devflowCore(pi: ExtensionAPI) {
2708
2643
  ): Promise<void> {
2709
2644
  const preflight = await runStartupPreflight(ctx, snapshot, options);
2710
2645
  if (!preflight.ok) return;
2646
+ if (snapshot.action === "run_parallel_window") {
2647
+ snapshot = await prepareParallelRun(ctx.cwd, snapshot);
2648
+ const feature = String(snapshot.current_feature ?? "");
2649
+ const step = String(compactStep(snapshot.current_step)?.id ?? "");
2650
+ if (!activeContract || activeContract.scope !== "parallel_window" || !(await contractIsFresh(ctx.cwd, activeContract))) {
2651
+ const activated = await activateExecutionContract(ctx, feature, step);
2652
+ if (!activated.ok) {
2653
+ parallelRunActive = false;
2654
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "并行窗口合同未激活" });
2655
+ ctx.ui.notify(`并行窗口合同未激活:${activated.reason}`, "info");
2656
+
2657
+ parallelRunActive = true;
2658
+ }
2659
+ }
2660
+ }
2711
2661
  setControlState(ctx, executingState(options.executingLabel));
2712
2662
  if (preflight.briefing) await triggerWorkflow(snapshot, source, undefined, preflight.briefing);
2713
2663
  else if (source === "start") await triggerWorkflow(snapshot, "start");
@@ -2719,6 +2669,22 @@ export default function devflowCore(pi: ExtensionAPI) {
2719
2669
  return { result, snapshot: parseJsonResult(result) };
2720
2670
  }
2721
2671
 
2672
+ async function runDynamicPlan(cwd: string, request: Record<string, unknown>): Promise<ProcessResult> {
2673
+ assertRuntimeProjectBoundary(cwd);
2674
+ return runProcess(["ruby", path.join(resolveKitRoot(cwd), "workflow-orchestrator/scripts/dynamic_plan.rb")], cwd, {}, JSON.stringify(request));
2675
+ }
2676
+
2677
+ async function startDynamicTurn(ctx: { ui: { notify(message: string, level: "info" | "error"): void } }, snapshot: Record<string, unknown>) {
2678
+ ctx.ui.notify(renderDynamicWorkflow(snapshot), "info");
2679
+ if (["ready", "cancelled"].includes(String(snapshot.delivery))) return;
2680
+ dynamicTurnRevision = Number(snapshot.revision);
2681
+ dynamicTurnProgress = String(snapshot.progress_key);
2682
+ activeContract = undefined;
2683
+ activeWorkflowTurn = true;
2684
+ controlState = executingState("执行中文工作流");
2685
+ await pi.sendUserMessage(dynamicWorkflowDirective(snapshot));
2686
+ }
2687
+
2722
2688
  async function inspectWorkflowWithRegistryRecovery(cwd: string): Promise<{ result: ProcessResult; snapshot: Record<string, unknown> | null }> {
2723
2689
  let inspected = await inspectWorkflow(cwd);
2724
2690
  const errors = Array.isArray(inspected.snapshot?.state_validation && (inspected.snapshot.state_validation as Record<string, unknown>).errors)
@@ -2818,7 +2784,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2818
2784
  ]));
2819
2785
  if (existing?.ok === true && existing.run) {
2820
2786
  parallelRunActive = true;
2821
- return { ...snapshot, parallel_run: existing.run };
2787
+ return refreshedParallelSnapshot(cwd, existing.run);
2822
2788
  }
2823
2789
  const revision = typeof snapshot.revision === "number" ? snapshot.revision : undefined;
2824
2790
  if (!feature || !workflowWindow || revision === undefined) throw new Error("parallel window snapshot is incomplete");
@@ -2834,7 +2800,18 @@ export default function devflowCore(pi: ExtensionAPI) {
2834
2800
  throw new Error(prepared.stderr || prepared.stdout || "parallel run prepare failed");
2835
2801
  }
2836
2802
  parallelRunActive = true;
2837
- return { ...snapshot, parallel_run: payload.run };
2803
+ return refreshedParallelSnapshot(cwd, payload.run);
2804
+ }
2805
+
2806
+ async function refreshedParallelSnapshot(cwd: string, run: unknown): Promise<Record<string, unknown>> {
2807
+ // prepare increments STATE. Recover the derived registry before building
2808
+ // the directive, including when resuming a previously stranded run.
2809
+ const fresh = await inspectWorkflowWithRegistryRecovery(cwd);
2810
+ if (!fresh.result.ok || fresh.snapshot?.action !== "run_parallel_window") {
2811
+ parallelRunActive = false;
2812
+ throw new Error(`并行准备后状态不可调度:${JSON.stringify(fresh.snapshot ?? fresh.result.stderr)}`);
2813
+ }
2814
+ return { ...fresh.snapshot, parallel_run: run };
2838
2815
  }
2839
2816
 
2840
2817
  async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string) {
@@ -2867,6 +2844,10 @@ export default function devflowCore(pi: ExtensionAPI) {
2867
2844
  snapshot: Record<string, unknown>,
2868
2845
  options: { doctorAlreadyPassed: boolean },
2869
2846
  ): Promise<WorkflowResumeDisposition> {
2847
+ if (snapshot.mode === "dynamic") {
2848
+ await startDynamicTurn(ctx, snapshot);
2849
+ return snapshot.delivery === "ready" ? "complete" : "execute";
2850
+ }
2870
2851
  const disposition = workflowResumeDisposition(snapshot);
2871
2852
  const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
2872
2853
  if (disposition === "execute") {
@@ -2905,60 +2886,15 @@ export default function devflowCore(pi: ExtensionAPI) {
2905
2886
  ui: { confirm(title: string, body: string): Promise<boolean>; notify(message: string, level: "info" | "error"): void; setStatus(key: string, text: string | undefined): void };
2906
2887
  }): Promise<boolean> {
2907
2888
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2908
- if (!inspected.result.ok || !inspected.snapshot || !isReviewableWorkflowSnapshot(inspected.snapshot)) return false;
2889
+ if (!inspected.result.ok || !inspected.snapshot || !isReviewableWorkflowSnapshot(inspected.snapshot, { allowAdvisory: true })) return false;
2909
2890
  const step = compactStep(inspected.snapshot.current_step);
2910
2891
  const stepId = typeof step?.id === "string" ? step.id : undefined;
2911
2892
  const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
2912
2893
  if (!stepId || revision === undefined) return false;
2913
2894
  const checkpoint = inspected.snapshot.status === "waiting_checkpoint";
2914
- let cp3Prerequisites: Array<{ id?: unknown; kind?: unknown; summary?: unknown }> = [];
2915
- if (checkpoint && inspected.snapshot.checkpoint === "CP-3") {
2916
- const feature = typeof inspected.snapshot.current_feature === "string" ? inspected.snapshot.current_feature : undefined;
2917
- if (!feature) return false;
2918
- const suite = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/acceptance_test_suite.rb", [
2919
- "inspect", "--feature", feature, "--json",
2920
- ]);
2921
- const suitePayload = parseJsonResult(suite);
2922
- if (!suite.ok || !suitePayload || suitePayload.ok !== true) {
2923
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "CP-3 前置检查失败" });
2924
- ctx.ui.notify(`无法读取 CP-3 测试前置事实:${suite.stderr || suite.stdout}`, "error");
2925
- return true;
2926
- }
2927
- cp3Prerequisites = Array.isArray(suitePayload.unconfirmed_prerequisites)
2928
- ? suitePayload.unconfirmed_prerequisites as Array<{ id?: unknown; kind?: unknown; summary?: unknown }>
2929
- : [];
2930
- }
2931
- const prerequisiteReview = cp3Prerequisites.length > 0
2932
- ? [
2933
- "",
2934
- `CP-3 检测到 ${cp3Prerequisites.length} 个尚未确认、但被候选验收测试引用的前置事实。批准后 Core 会在同一原子事务中确认它们并冻结测试集:`,
2935
- ...cp3Prerequisites.map(item => `- ${String(item.id ?? "?")} [${String(item.kind ?? "?")}]${item.summary ? `:${String(item.summary)}` : ""}`),
2936
- ].join("\n")
2937
- : "";
2938
- const approved = await ctx.ui.confirm(
2939
- "确认推进 Devflow 工作流",
2940
- `当前步骤:${stepId}\n\n步骤产物已生成并通过基础校验。确认${checkpoint ? "当前 checkpoint 并" : ""}推进到下一步骤吗?${prerequisiteReview}`,
2941
- );
2942
- if (!approved) {
2943
- setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
2944
- ctx.ui.notify("已保留当前步骤,未推进工作流。", "info");
2945
- return true;
2946
- }
2947
- if (checkpoint && stepId === "wireframe-review") {
2948
- const feature = inspected.snapshot.current_feature;
2949
- if (typeof feature !== "string") return false;
2950
- const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
2951
- wireframeApprovalArgs(feature, revision));
2952
- if (!controlPlaneSucceeded(approval)) {
2953
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
2954
- ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
2955
- return true;
2956
- }
2957
- }
2958
2895
  const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
2959
2896
  "--write", "--json", "--expected-revision", String(revision),
2960
2897
  ...(checkpoint ? ["--confirm-checkpoint", "--runtime-confirmed"] : ["--complete-step", stepId]),
2961
- ...(cp3Prerequisites.length > 0 ? ["--confirm-test-prerequisites"] : []),
2962
2898
  ]);
2963
2899
  if (!result.ok || !controlPlaneSucceeded(result)) {
2964
2900
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
@@ -3093,15 +3029,27 @@ export default function devflowCore(pi: ExtensionAPI) {
3093
3029
  ctx.ui.notify("当前步骤正在执行,等这一轮结束后再说「下一步」。", "info");
3094
3030
  return;
3095
3031
  }
3032
+ const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
3033
+ if (dynamic?.mode === "dynamic") {
3034
+ await startDynamicTurn(ctx, dynamic);
3035
+ return;
3036
+ }
3096
3037
  if (mode === "next") {
3097
3038
  const report = await runDoctor(ctx);
3098
3039
  if (!report.ok) {
3099
- ctx.ui.notify(`运行环境检查没有通过,暂时不能推进:\n${formatDoctorReport(report).slice(0, 6000)}`, "error");
3100
- return;
3040
+ ctx.ui.notify(`${"软门禁提示"}:\n${formatDoctorReport(report).slice(0, 6000)}`, "info");
3041
+
3101
3042
  }
3102
3043
  }
3044
+ // In advisory mode "next" is the user's request to continue, including
3045
+ // when the current output is missing or a checker failed.
3046
+ if (mode === "next") {
3047
+ const current = await diagnoseCurrentWorkflow(ctx.cwd, { skipWorkflowUpgrade: true });
3048
+ if (current.snapshot && !current.diagnosis.diagnoses.some(item => item.code === "ROLLOVER_PENDING")
3049
+ && await confirmAndAdvanceWorkflowStep(ctx)) return;
3050
+ }
3103
3051
  // Declining the template upgrade must not strand the step behind it.
3104
- let skipWorkflowUpgrade = false;
3052
+ let skipWorkflowUpgrade = true;
3105
3053
  for (let attempt = 0; attempt < 4; attempt += 1) {
3106
3054
  const current = await diagnoseCurrentWorkflow(ctx.cwd, { skipWorkflowUpgrade });
3107
3055
  const { diagnosis, snapshot } = current;
@@ -3536,11 +3484,11 @@ export default function devflowCore(pi: ExtensionAPI) {
3536
3484
  parallelDispatchFailureThisTurn = result.stderr || result.stdout || `parallel ${action} failed`;
3537
3485
  return;
3538
3486
  }
3539
- if (action !== "complete") return;
3487
+
3540
3488
  const refreshed = parseJsonResult(await runRuby(projectRoot, "workflow-orchestrator/scripts/parallel_run.rb", ["--action", "inspect", "--feature", feature, "--json"]));
3541
3489
  const current = refreshed?.run as Record<string, unknown> | undefined;
3542
3490
  const branches = Array.isArray(current?.branches) ? current.branches as Array<Record<string, unknown>> : [];
3543
- if (branches.length > 0 && branches.every(branch => branch.status === "succeeded")) {
3491
+ if (branches.length > 0 && branches.every(branch => branch.status === "succeeded" || (["failed", "aborted"].includes(String(branch.status))))) {
3544
3492
  const after = await inspectWorkflow(projectRoot);
3545
3493
  const joinRevision = Number(after.snapshot?.revision);
3546
3494
  if (Number.isInteger(joinRevision)) {
@@ -3702,8 +3650,8 @@ export default function devflowCore(pi: ExtensionAPI) {
3702
3650
  if (cliSessionResumeRequested && controlState.phase === "idle" && !pendingRollover) {
3703
3651
  const report = await runDoctor(ctx);
3704
3652
  if (!report.ok) {
3705
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
3706
- return;
3653
+ ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "info");
3654
+
3707
3655
  }
3708
3656
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
3709
3657
  if (!inspected.result.ok || !inspected.snapshot) {
@@ -3790,6 +3738,25 @@ export default function devflowCore(pi: ExtensionAPI) {
3790
3738
 
3791
3739
  pi.on("agent_end", async (event, ctx) => {
3792
3740
  if (!activeWorkflowTurn || event.willContinue) return;
3741
+ if (dynamicTurnRevision !== undefined) {
3742
+ const before = dynamicTurnRevision;
3743
+ dynamicTurnRevision = undefined;
3744
+ activeWorkflowTurn = false;
3745
+ const snapshot = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
3746
+ if (snapshot?.mode === "dynamic") {
3747
+ ctx.ui.notify(renderDynamicWorkflow(snapshot), "info");
3748
+ if (["run_task", "plan_work", "reconcile_plan"].includes(String(snapshot.action)) && Number(snapshot.revision) > before && snapshot.progress_key !== dynamicTurnProgress) {
3749
+ dynamicTurnRevision = Number(snapshot.revision);
3750
+ dynamicTurnProgress = String(snapshot.progress_key);
3751
+ activeWorkflowTurn = true;
3752
+ setControlState(ctx, executingState("继续执行中文工作流"));
3753
+ pi.sendMessage({ customType: "devflow-dynamic-continue", content: dynamicWorkflowDirective(snapshot), display: false }, { triggerTurn: true, deliverAs: "nextTurn" });
3754
+ } else {
3755
+ setControlState(ctx, snapshot.delivery === "ready" ? initialControlState() : waitingUserState("保留当前结果;等待决定、外部条件或执行恢复"));
3756
+ }
3757
+ return;
3758
+ }
3759
+ }
3793
3760
  const assistant = [...event.messages].reverse().find(message => (
3794
3761
  message && typeof message === "object" && (message as { role?: unknown }).role === "assistant"
3795
3762
  ));
@@ -3806,7 +3773,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3806
3773
  parallelTaskContexts.clear();
3807
3774
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "并行子 Agent 未启动" });
3808
3775
  ctx.ui.notify(
3809
- `并行窗口未启动任何子 Agent,已停止等待不存在的生命周期事件;STATE 未推进。${parallelDispatchFailureThisTurn ? `\n原因:${parallelDispatchFailureThisTurn}` : ""}\n请检查打包 Agent 注册;若当前环境不支持并行,调度器必须按 serial_fallback_order 串行执行。`,
3776
+ `并行窗口未启动任何子 Agent,已停止等待不存在的生命周期事件;STATE 未推进。${parallelDispatchFailureThisTurn ? `\n原因:${parallelDispatchFailureThisTurn}` : ""}\n请先检查本回合的状态、合同与分支派发阻塞证据;未派发不代表 Agent 未安装。仅确认环境不支持并行时才按 serial_fallback_order 串行执行。`,
3810
3777
  "error",
3811
3778
  );
3812
3779
  return;
@@ -3823,6 +3790,10 @@ export default function devflowCore(pi: ExtensionAPI) {
3823
3790
  // UI confirmation may legitimately wait for the user longer than the
3824
3791
  // host's event-handler deadline. Do not await it from agent_end or the
3825
3792
  // host reports a timeout and the approval result is lost.
3793
+ {
3794
+ setControlState(ctx, waitingUserState("当前步骤结束;说「下一步」即可继续,检查缺项仅作提示"));
3795
+ return;
3796
+ }
3826
3797
  void confirmAndAdvanceWorkflowStep(ctx).catch(error => {
3827
3798
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进确认处理失败" });
3828
3799
  ctx.ui.notify(`推进确认处理失败:${String(error)}`, "error");
@@ -3832,6 +3803,17 @@ export default function devflowCore(pi: ExtensionAPI) {
3832
3803
 
3833
3804
  pi.on("input", async (event, ctx) => {
3834
3805
  if (event.source !== "interactive") return;
3806
+ if (event.text.trim() && !event.text.trim().startsWith("/") && !classifyNextStepPhrase(event.text)) {
3807
+ const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
3808
+ if (dynamic?.mode === "dynamic") {
3809
+ dynamicTurnRevision = Number(dynamic.revision);
3810
+ dynamicTurnProgress = String(dynamic.progress_key);
3811
+ activeWorkflowTurn = true;
3812
+ activeContract = undefined;
3813
+ setControlState(ctx, executingState("处理需求调整"));
3814
+ return { text: `${dynamicWorkflowDirective(dynamic)}\n用户补充:${event.text}`, handled: false };
3815
+ }
3816
+ }
3835
3817
  const controlAction = classifyNaturalControlAction(event.text);
3836
3818
  const stepNote = event.text.trim();
3837
3819
  if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/") && !classifyNextStepPhrase(stepNote)) {
@@ -3940,15 +3922,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3940
3922
  ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
3941
3923
  return { handled: true };
3942
3924
  }
3943
- if (checkpoint && stepId === "wireframe-review" && typeof inspected.snapshot.current_feature === "string") {
3944
- const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
3945
- wireframeApprovalArgs(inspected.snapshot.current_feature, revision));
3946
- if (!controlPlaneSucceeded(approval)) {
3947
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
3948
- ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
3949
- return { handled: true };
3950
- }
3951
- }
3925
+
3952
3926
  const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
3953
3927
  "--write",
3954
3928
  "--json",
@@ -4033,6 +4007,19 @@ export default function devflowCore(pi: ExtensionAPI) {
4033
4007
  });
4034
4008
 
4035
4009
  pi.on("tool_call", async (event, ctx) => {
4010
+ if (dynamicTurnRevision !== undefined) {
4011
+ const name = String(event.toolName).toLowerCase();
4012
+ if (isDirectControlMutation(name, event.input, ctx.cwd) || isShellControlMutation(name, event.input) || name === "devflow_transition") {
4013
+ return { block: true, reason: "动态计划由 Core 独占写入;请使用 devflow_plan,不能手工写入通过状态。" };
4014
+ }
4015
+ if (name === "task" && requestedTaskAgents(event.input).names.includes("devflow-code-reviewer")) {
4016
+ const snapshot = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
4017
+ const task = snapshot?.current_step as Record<string, unknown> | undefined;
4018
+ if (task?.kind === "review" && task.status === "running") dynamicReviewBindings.set(event.toolCallId, { taskId: String(task.id), attempt: String(task.attempt), revision: Number(snapshot?.revision) });
4019
+ }
4020
+ return;
4021
+ }
4022
+ const decision = await (async () => {
4036
4023
  const toolName = String(event.toolName).toLowerCase();
4037
4024
  const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
4038
4025
  const toolCallId = String(event.toolCallId ?? "");
@@ -4048,11 +4035,6 @@ export default function devflowCore(pi: ExtensionAPI) {
4048
4035
  const feature = typeof toolInput?.feature === "string" ? toolInput.feature.trim() : "";
4049
4036
  const phase = typeof toolInput?.phase === "string" && toolInput.phase.trim() ? toolInput.phase.trim() : "默认需求阶段";
4050
4037
  if (!feature) return { block: true, reason: "bootstrap requires a feature name" };
4051
- const confirmed = await ctx.ui.confirm(
4052
- "确认启动新的 Devflow Feature",
4053
- `Feature:${feature}\n阶段:${phase}\n\n确认后由 Core 原子初始化并切换当前 Feature;不会由 AI 直接修改 STATE.md。若当前仍有未完成 Feature,Core 会拒绝切换并保持原状态。继续吗?`,
4054
- );
4055
- if (!confirmed) return { block: true, reason: "User declined runtime bootstrap authorization." };
4056
4038
  if (!toolCallId) return { block: true, reason: "bootstrap runtime authorization is missing a tool-call binding" };
4057
4039
  runtimeBootstrapAuthorizations.add(toolCallId);
4058
4040
  bootstrapAuthorized = true;
@@ -4061,14 +4043,7 @@ export default function devflowCore(pi: ExtensionAPI) {
4061
4043
  const branchContext = parallelBranchContext(event.input);
4062
4044
  if (branchContext) parallelTaskContexts.set(event.toolCallId, branchContext);
4063
4045
  }
4064
- if (coreHardGatesEnabled(activeContract) && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
4065
- if (!activeContract.allowedTools.includes(toolName)) {
4066
- return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
4067
- }
4068
- if (!(await contractIsFresh(ctx.cwd, activeContract))) {
4069
- return { block: true, reason: `Active contract ${activeContract.contractId} is stale against STATE or graph revision; synthesize a new contract before continuing.` };
4070
- }
4071
- }
4046
+
4072
4047
  if (toolName === "task") {
4073
4048
  const requested = requestedTaskAgents(event.input);
4074
4049
  if (requested.missing) {
@@ -4094,43 +4069,7 @@ export default function devflowCore(pi: ExtensionAPI) {
4094
4069
  if (controlState.phase === "planning" && !allowedInDraft) {
4095
4070
  return { block: true, reason: planningToolBlockReason(toolName) };
4096
4071
  }
4097
- if (
4098
- coreHardGatesEnabled(activeContract) &&
4099
- activeContract &&
4100
- controlState.phase === "executing" &&
4101
- !requested.names.every(agent => activeContract?.allowedAgents.includes(agent))
4102
- ) {
4103
- return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
4104
- }
4105
- if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && contractAgentCalls + requested.names.length > activeContract.toolBudget) {
4106
- return { block: true, reason: `Active contract ${activeContract.contractId} exhausted its fixed agent budget (${activeContract.toolBudget}).` };
4107
- }
4108
- if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && activeContract.step === "code-review" && requested.names.length !== 1) {
4109
- return { block: true, reason: "Code review requires exactly one independent reviewer task." };
4110
- }
4111
- if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
4112
- const preflight = await runProcess([
4113
- "python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
4114
- "--root", ctx.cwd,
4115
- "--feature", activeContract.feature,
4116
- "--step", activeContract.step,
4117
- "--contract", `.devflow/planning/contracts/${activeContract.feature}/${activeContract.contractId}.json`,
4118
- "--obligations", "[]",
4119
- "--review-agent", "devflow-code-reviewer",
4120
- "--stage", "entry",
4121
- ], ctx.cwd);
4122
- let result: Record<string, unknown> | null = null;
4123
- try {
4124
- result = JSON.parse(preflight.stdout) as Record<string, unknown>;
4125
- } catch {
4126
- // An unparseable Core preflight result cannot authorize a reviewer task.
4127
- }
4128
- if (!preflight.ok || result?.status !== "ready") {
4129
- return { block: true, reason: `Code review task preflight did not pass: ${preflight.stdout || preflight.stderr || "unavailable"}` };
4130
- }
4131
- if (!toolCallId) return { block: true, reason: "Code review task needs a runtime tool-call identity." };
4132
- pendingReviewTasks.set(toolCallId, { feature: activeContract.feature, contractId: activeContract.contractId });
4133
- }
4072
+
4134
4073
  if (activeContract && controlState.phase === "executing") contractAgentCalls += requested.names.length;
4135
4074
  } else if (
4136
4075
  controlState.phase === "planning" &&
@@ -4160,41 +4099,7 @@ export default function devflowCore(pi: ExtensionAPI) {
4160
4099
  "Devflow 状态迁移只能由运行时在用户明确批准已展示的步骤或 checkpoint 后执行;LLM 不得自行推进。",
4161
4100
  };
4162
4101
  }
4163
- if (isUserConfirmedStepTransition && activeContract && coreHardGatesEnabled(activeContract)) {
4164
- const evidence = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/machine_evidence.rb", [
4165
- "--feature", activeContract.feature, "--step", activeContract.step, "--write", "--json",
4166
- ]);
4167
- const evidencePayload = parseJsonResult(evidence);
4168
- if (!evidence.ok || !evidencePayload || evidencePayload.complete !== true) {
4169
- return {
4170
- block: true,
4171
- reason: `Devflow machine evidence is incomplete; collect independent runtime evidence before completing ${activeContract.step}.`,
4172
- };
4173
- }
4174
- }
4175
- if (coreHardGatesEnabled(activeContract) && activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
4176
- const inspected = await inspectWorkflow(ctx.cwd);
4177
- if (!inspected.result.ok || !inspected.snapshot) {
4178
- return {
4179
- block: true,
4180
- reason: `Active Devflow workflow mutation blocked because authoritative state could not be inspected: ${inspected.result.stderr || inspected.result.stdout}`,
4181
- };
4182
- }
4183
- const fresh = activeContract ? await contractIsFresh(ctx.cwd, activeContract) : false;
4184
- const reason = stepScopedMutationBlockReason(toolName, event.input, ctx.cwd, inspected.snapshot, activeContract, fresh);
4185
- if (reason) return { block: true, reason };
4186
- }
4187
- if (
4188
- coreHardGatesEnabled(activeContract) &&
4189
- !toolName.startsWith("devflow_") &&
4190
- ["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
4191
- inputPaths(event.input, ctx.cwd).some(isCoreOwnedGovernancePath)
4192
- ) {
4193
- return {
4194
- block: true,
4195
- reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
4196
- };
4197
- }
4102
+
4198
4103
  // Feature artifacts written while no step is running are never adopted by
4199
4104
  // the workflow (zhaoshang, 2026-10-22). Refuse early in the interactive
4200
4105
  // main session; subagents (no UI), review-feedback edits and controlled
@@ -4229,31 +4134,8 @@ export default function devflowCore(pi: ExtensionAPI) {
4229
4134
  }
4230
4135
  }
4231
4136
  if (toolName.startsWith("devflow_")) return;
4232
- if (
4233
- coreHardGatesEnabled(activeContract) &&
4234
- activeContract &&
4235
- controlState.phase === "executing" &&
4236
- !internalControlResource &&
4237
- ["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
4238
- ) {
4239
- const paths = inputPaths(event.input, ctx.cwd);
4240
- if (paths.length === 0 || paths.some(filePath => !contractAllowsPath(activeContract!, filePath))) {
4241
- return {
4242
- block: true,
4243
- reason: `Active contract ${activeContract.contractId} only allows edits under: ${activeContract.allowedPaths.join(", ")}`,
4244
- };
4245
- }
4246
- }
4247
- if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && ["bash", "shell", "exec_command"].includes(toolName)) {
4248
- const command = String((event.input as Record<string, unknown> | undefined)?.command ?? (event.input as Record<string, unknown> | undefined)?.cmd ?? "");
4249
- const shell = shellMutationPaths(command, ctx.cwd);
4250
- if (shell.mutating && (shell.paths.length === 0 || shell.paths.some(filePath => !contractAllowsPath(activeContract!, filePath)))) {
4251
- return {
4252
- block: true,
4253
- reason: `Active contract ${activeContract.contractId} 不允许通过 Bash/脚本修改合同外路径;请使用合同允许路径,或先更新合同。`,
4254
- };
4255
- }
4256
- }
4137
+
4138
+
4257
4139
  if (isDirectControlMutation(toolName, event.input, ctx.cwd) || isShellControlMutation(toolName, event.input)) {
4258
4140
  return {
4259
4141
  block: true,
@@ -4277,8 +4159,9 @@ export default function devflowCore(pi: ExtensionAPI) {
4277
4159
  if (gate.stdout) {
4278
4160
  try {
4279
4161
  const payload = JSON.parse(gate.stdout) as {
4280
- hookSpecificOutput?: { permissionDecision?: string; permissionDecisionReason?: string };
4162
+ hookSpecificOutput?: { permissionDecision?: string; permissionDecisionReason?: string; additionalContext?: string };
4281
4163
  };
4164
+ if (payload.hookSpecificOutput?.additionalContext) ctx.ui.notify(payload.hookSpecificOutput.additionalContext, "info");
4282
4165
  if (payload.hookSpecificOutput?.permissionDecision === "deny") {
4283
4166
  return {
4284
4167
  block: true,
@@ -4292,9 +4175,27 @@ export default function devflowCore(pi: ExtensionAPI) {
4292
4175
  };
4293
4176
  }
4294
4177
  }
4178
+ })();
4179
+ if (decision?.block && !String(decision.reason).startsWith("bootstrap ") && decision.reason !== "User declined runtime bootstrap authorization.") {
4180
+ ctx.ui.notify(`软门禁提示:${decision.reason}`, "info");
4181
+ return;
4182
+ }
4183
+ return decision;
4295
4184
  });
4296
4185
 
4297
4186
  pi.on("tool_result", async (event, ctx) => {
4187
+ const dynamicBinding = dynamicReviewBindings.get(event.toolCallId);
4188
+ if (event.toolName === "task" && dynamicBinding) {
4189
+ dynamicReviewBindings.delete(event.toolCallId);
4190
+ const results = (event.details as { results?: Array<Record<string, unknown>> } | undefined)?.results;
4191
+ const child = results?.length === 1 ? results[0] : undefined;
4192
+ if (!event.isError && child?.agent === "devflow-code-reviewer" && child.exitCode === 0 && child.truncated === false && typeof child.output === "string") {
4193
+ try {
4194
+ const report = JSON.parse(child.output);
4195
+ if (typeof report.summary === "string" && Array.isArray(report.findings)) dynamicReviewResults.set(event.toolCallId, { ...dynamicBinding, report });
4196
+ } catch { /* Invalid reviewer output never becomes a pass. */ }
4197
+ }
4198
+ }
4298
4199
  if (event.toolName === "task") {
4299
4200
  const binding = pendingReviewTasks.get(event.toolCallId);
4300
4201
  if (binding) {
@@ -4485,20 +4386,13 @@ export default function devflowCore(pi: ExtensionAPI) {
4485
4386
  if (params.expectedRevision === undefined || !params.delta) {
4486
4387
  return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "propose requires expectedRevision and delta" });
4487
4388
  }
4488
- if (coreHardGatesEnabled(activeContract) && params.delta.kind === "test_case" && activeContract?.step !== "acceptance-test-design") {
4489
- return renderResult({
4490
- ok: false,
4491
- exitCode: 1,
4492
- stdout: "",
4493
- stderr: "TC candidates are accepted only during the active acceptance-test-design contract.",
4494
- });
4495
- }
4389
+
4496
4390
  args.push("--expected-revision", String(params.expectedRevision), "--delta-json", JSON.stringify(params.delta));
4497
4391
  // `write` is an explicit dry-run switch. Omit it only when the caller
4498
4392
  // explicitly asks for a preview; the normal approved proposal remains
4499
4393
  // a Core-owned write.
4500
4394
  if (params.write !== false) args.push("--write");
4501
- if (coreHardGatesEnabled(activeContract) && activeContract?.allowedGraphDelta?.length) args.push("--allowed-kinds", activeContract.allowedGraphDelta.join(","));
4395
+
4502
4396
  }
4503
4397
  if (params.action === "confirm") {
4504
4398
  if (params.expectedRevision === undefined || !params.nodeId || !params.evidence) {
@@ -4602,9 +4496,7 @@ export default function devflowCore(pi: ExtensionAPI) {
4602
4496
  }),
4603
4497
  approval: args => args.action === "validate" ? "read" : "write",
4604
4498
  async execute(_id, params, _signal, _onUpdate, ctx) {
4605
- if (coreHardGatesEnabled(activeContract) && params.action !== "validate" && activeContract?.step !== "acceptance-test-design") {
4606
- return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "API test matrix compilation/import is allowed only during the active acceptance-test-design contract." });
4607
- }
4499
+
4608
4500
  if (params.action === "validate") {
4609
4501
  const process = await runPython(ctx.cwd, "workflow-orchestrator/scripts/api_test_matrix.py", ["validate", "--feature", params.feature, "--json"]);
4610
4502
  const payload = parseJsonResult(process);
@@ -4767,7 +4659,7 @@ export default function devflowCore(pi: ExtensionAPI) {
4767
4659
  "Actions: validate (bindings and suite), validate_human (manual TCs), run (executes every automated TC and writes the canonical acceptance-results.json; Core is the only writer), validate_results (re-checks recorded results against current approvals and hashes), gc_runs (prunes .devflow/runs/<feature> execution directories per retention rules; dry-run by default, pass write=true to delete; optional retentionDays / unreferencedTtlDays).",
4768
4660
  "Failure analysis reads only the Core-produced result.json summary; trace files and screenshots are not readable by Agent tools and are for the user to inspect locally with Playwright in a terminal.",
4769
4661
  "devflow_playwright bindings require CURRENT human approvals created with devflow_approval and confirmed by the user in /devflow control: an execution_plan approval per TC (pins the DSL, TC and suite) and an environment_profile approval for the target profile; manual or downgraded TCs need a human_result / mode_override approval. You cannot confirm approvals yourself.",
4770
- "Hard gate: a missing, superseded or expired approval, or a DSL/profile/suite/evidence hash that changed after the approval or the run, makes run and validate_results return ok=false and blocks run-acceptance-tests from completing; there is no soft override and results must never be written by hand.",
4662
+ "A missing approval or changed DSL/profile/suite/evidence hash is reported as ok=false by run and validate_results. In soft gate mode these findings do not block workflow advancement. Failed or unexecuted tests must never be reported as passed; results must never be written by hand.",
4771
4663
  ].join(" "),
4772
4664
  parameters: z.object({
4773
4665
  action: z.enum(["validate", "validate_human", "run", "validate_results", "gc_runs"]),
@@ -5214,6 +5106,59 @@ export default function devflowCore(pi: ExtensionAPI) {
5214
5106
  },
5215
5107
  });
5216
5108
 
5109
+ pi.registerTool({
5110
+ name: "devflow_plan",
5111
+ label: "Devflow 中文工作流",
5112
+ description: "Inspect or execute the Core-owned dynamic plan. Never accepts caller-written pass/approval results. Use revise for task proposals, claim before work, report for activity results, verify for actual executable checks, review_result for a captured independent review, and decision only for genuine user choices. Task fields: id,title,kind(discover/implement/verify/review),requirements(R-ids),dependencies,inputs,outputs,checks([{argv:string[],timeout?:number}]),cancelled?,reason?.",
5113
+ parameters: z.object({
5114
+ action: z.enum(["inspect", "revise", "claim", "report", "verify", "review_result", "recover", "wait", "resume", "decision"]),
5115
+ expected_revision: z.number().int().optional(),
5116
+ task_id: z.string().optional(),
5117
+ attempt: z.string().optional(),
5118
+ tasks: z.array(z.object({
5119
+ id: z.string(), title: z.string(), kind: z.enum(["discover", "implement", "repair", "verify", "review"]),
5120
+ requirements: z.array(z.string()), dependencies: z.array(z.string()), inputs: z.array(z.string()), outputs: z.array(z.string()),
5121
+ checks: z.array(z.object({ argv: z.array(z.string()).min(1), timeout: z.number().int().optional() })),
5122
+ cancelled: z.boolean().optional(), reason: z.string().optional(),
5123
+ })).optional(),
5124
+ reason: z.string().optional(), summary: z.string().optional(), task_call_id: z.string().optional(),
5125
+ question: z.string().optional(), options: z.array(z.string()).optional(),
5126
+ task_ids: z.array(z.string()).optional(),
5127
+ scope_decision_id: z.string().optional(), remove_requirements: z.array(z.string()).optional(), approval_option: z.string().optional(),
5128
+ }),
5129
+ approval: "write",
5130
+ async execute(_id, rawParams, _signal, _onUpdate, ctx) {
5131
+ const params = rawParams as { action: string; task_call_id?: string; task_id?: string; attempt?: string; expected_revision?: number; question?: string };
5132
+ const request: Record<string, unknown> = { ...params };
5133
+ if (params.action === "review_result") {
5134
+ const captured = params.task_call_id ? dynamicReviewResults.get(params.task_call_id) : undefined;
5135
+ if (!captured || captured.taskId !== params.task_id || captured.attempt !== params.attempt || captured.revision !== params.expected_revision) {
5136
+ return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "No current independent reviewer result is bound to this task attempt. Dispatch the read-only reviewer first." });
5137
+ }
5138
+ request.runtime_review = true;
5139
+ request.report = captured.report;
5140
+ }
5141
+ let result = await runDynamicPlan(ctx.cwd, request);
5142
+ let snapshot = parseJsonResult(result);
5143
+ if (snapshot?.ok === true && params.action === "review_result" && params.task_call_id) dynamicReviewResults.delete(params.task_call_id);
5144
+ if (snapshot?.ok === true && params.action === "decision") {
5145
+ const decisions = snapshot.decisions as Array<Record<string, unknown>>;
5146
+ const pending = decisions.find(item => item.status === "pending" && item.question === params.question);
5147
+ if (pending) {
5148
+ const removals = pending.remove_requirements as string[] | undefined;
5149
+ const question = String(pending.question) + (removals?.length ? `\n拟移除验收要求:${removals.join("、")};仅选择「${pending.approval_option}」会授权移除。` : "");
5150
+ const choice = await ctx.ui.select(question, pending.options as string[]);
5151
+ if (choice) {
5152
+ result = await runDynamicPlan(ctx.cwd, { action: "decide", expected_revision: snapshot.revision, decision_id: pending.id, choice, runtime_decision: true });
5153
+ snapshot = parseJsonResult(result);
5154
+ }
5155
+ }
5156
+ }
5157
+ if (snapshot?.mode === "dynamic") ctx.ui.setWidget?.("devflow-plan", projectControlWidget(renderDynamicWorkflow(snapshot).split("\n")));
5158
+ return renderResult(result);
5159
+ },
5160
+ });
5161
+
5217
5162
  pi.registerTool({
5218
5163
  name: "devflow_bootstrap",
5219
5164
  label: "Devflow Bootstrap",
@@ -5221,11 +5166,24 @@ export default function devflowCore(pi: ExtensionAPI) {
5221
5166
  parameters: z.object({
5222
5167
  feature: z.string().min(1),
5223
5168
  phase: z.string().min(1).optional(),
5224
- flow: z.enum(["feature-development", "acceptance-remediation", "regression-acceptance"]).default("feature-development"),
5169
+ flow: z.enum(["dynamic", "feature-development", "acceptance-remediation", "regression-acceptance"]).default("dynamic"),
5225
5170
  force: z.boolean().default(false),
5226
5171
  }),
5227
5172
  approval: "write",
5228
- async execute(_id, params, _signal, _onUpdate, ctx) {
5173
+ async execute(_id, rawParams, _signal, _onUpdate, ctx) {
5174
+ const params = rawParams as { flow: string; feature: string; phase?: string; force: boolean };
5175
+ if (params.flow === "dynamic") {
5176
+ const result = await runDynamicPlan(ctx.cwd, { action: "initialize", feature: params.feature, phase: params.phase });
5177
+ const snapshot = parseJsonResult(result);
5178
+ if (snapshot?.mode === "dynamic") {
5179
+ dynamicTurnRevision = Number(snapshot.revision);
5180
+ dynamicTurnProgress = String(snapshot.progress_key);
5181
+ activeWorkflowTurn = true;
5182
+ activeContract = undefined;
5183
+ setControlState(ctx, executingState("梳理需求并生成中文步骤"));
5184
+ }
5185
+ return renderResult(result);
5186
+ }
5229
5187
  // Consume the one-shot runtime authorization before running Core's
5230
5188
  // atomic bootstrap sequence. A retry must obtain a fresh user approval.
5231
5189
  runtimeBootstrapAuthorizations.delete(String(_id));
@@ -5320,9 +5278,7 @@ export default function devflowCore(pi: ExtensionAPI) {
5320
5278
  if (!params.step) {
5321
5279
  return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "step is required" });
5322
5280
  }
5323
- if (params.confirm !== true) {
5324
- return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "complete-step requires explicit user confirmation (confirm=true)" });
5325
- }
5281
+
5326
5282
  const snapshot = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
5327
5283
  const currentStep = snapshot?.current_step && typeof snapshot.current_step === "object"
5328
5284
  ? (snapshot.current_step as Record<string, unknown>).id
@@ -5341,9 +5297,7 @@ export default function devflowCore(pi: ExtensionAPI) {
5341
5297
  if (activeContract?.feature === feature && activeContract.step === params.step && activeContract.contractPath) evidenceArgs.push("--contract", activeContract.contractPath);
5342
5298
  const evidence = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/machine_evidence.rb", evidenceArgs);
5343
5299
  const evidencePayload = parseJsonResult(evidence);
5344
- if (!controlPlaneSucceeded(evidence) || evidencePayload?.complete !== true) {
5345
- return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "machine evidence collection failed or required outputs are incomplete" }, { action: params.action, evidence: evidence.stdout });
5346
- }
5300
+
5347
5301
  }
5348
5302
  args.push("--complete-step", params.step);
5349
5303
  } else if (params.action === "confirm-checkpoint") {
@@ -5598,6 +5552,15 @@ export default function devflowCore(pi: ExtensionAPI) {
5598
5552
 
5599
5553
  const devflowCommand = async (args: string, ctx: DevflowCommandContext): Promise<void> => {
5600
5554
  const trimmed = args.trim();
5555
+ if (["", "status", "control", "explain-state"].includes(trimmed)) {
5556
+ const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
5557
+ if (dynamic?.mode === "dynamic") {
5558
+ const lines = renderDynamicWorkflow(dynamic, trimmed === "control").split("\n");
5559
+ ctx.ui.notify(lines.join("\n"), "info");
5560
+ ctx.ui.setWidget?.("devflow-plan", projectControlWidget(lines));
5561
+ return;
5562
+ }
5563
+ }
5601
5564
  if (trimmed === "" || trimmed === "status") {
5602
5565
  await showWorkflowOverview(ctx);
5603
5566
  return;
@@ -5670,6 +5633,17 @@ export default function devflowCore(pi: ExtensionAPI) {
5670
5633
  if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
5671
5634
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5672
5635
  const snapshot = inspected.snapshot;
5636
+ if (snapshot?.mode === "dynamic") {
5637
+ const result = await runDynamicPlan(ctx.cwd, { action: "cancel", expected_revision: snapshot.revision, runtime_cancel: true, reason: trimmed.replace(/^\S+\s*/, "") || "用户主动终止当前需求" });
5638
+ const cancelled = parseJsonResult(result);
5639
+ if (cancelled?.ok) {
5640
+ dynamicTurnRevision = undefined;
5641
+ activeWorkflowTurn = false;
5642
+ setControlState(ctx, initialControlState());
5643
+ ctx.ui.notify("已取消需求,保留已有工作与失败证据。", "info");
5644
+ } else ctx.ui.notify(String(cancelled?.reason ?? result.stderr), "error");
5645
+ return;
5646
+ }
5673
5647
  const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
5674
5648
  const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
5675
5649
  const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
@@ -5734,8 +5708,8 @@ export default function devflowCore(pi: ExtensionAPI) {
5734
5708
  if (trimmed === "resume") {
5735
5709
  const report = await runDoctor(ctx);
5736
5710
  if (!report.ok) {
5737
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
5738
- return;
5711
+ ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "info");
5712
+
5739
5713
  }
5740
5714
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5741
5715
  if (!inspected.result.ok || !inspected.snapshot) {
@@ -5773,100 +5747,19 @@ export default function devflowCore(pi: ExtensionAPI) {
5773
5747
  }
5774
5748
  if (trimmed.startsWith("start ")) {
5775
5749
  const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
5776
- if (!feature) {
5777
- ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
5778
- return;
5779
- }
5780
- const phase = requestedPhase || defaultPhaseId(feature);
5781
- if (!validPhaseId(phase)) {
5782
- ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
5750
+ if (!feature || (requestedPhase && !validPhaseId(requestedPhase))) {
5751
+ ctx.ui.notify("用法:/devflow start <需求名>|<可选阶段标识>", "error");
5783
5752
  return;
5784
5753
  }
5785
5754
  const report = await runDoctor(ctx);
5786
- if (!report.ok) {
5787
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
5788
- return;
5789
- }
5790
- const existing = await inspectWorkflow(ctx.cwd);
5791
- const existingFeature = existing.snapshot?.current_feature;
5792
- const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
5793
- if (
5794
- !previousWorkflowComplete &&
5795
- typeof existingFeature === "string" &&
5796
- existingFeature !== feature
5797
- ) {
5798
- ctx.ui.notify(
5799
- `当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
5800
- "error",
5801
- );
5802
- return;
5803
- }
5804
- // This dry-run happens before init_workflow.rb, so a rejected feature
5805
- // can never overwrite generated workflow files belonging to an active
5806
- // owner or a legacy phase directory that has no trustworthy owner.
5807
- const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
5808
- "claim",
5809
- "--feature",
5810
- feature,
5811
- "--phase",
5812
- phase,
5813
- "--flow",
5814
- "feature-development",
5815
- "--json",
5816
- ]);
5817
- if (!controlPlaneSucceeded(ownership)) {
5818
- ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
5819
- return;
5820
- }
5821
- const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
5822
- "--write",
5823
- "--json",
5824
- ]);
5825
- if (!controlPlaneSucceeded(workflow)) {
5826
- ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
5755
+ if (!report.ok) ctx.ui.notify(formatDoctorReport(report).slice(0, 6000), "info");
5756
+ const result = await runDynamicPlan(ctx.cwd, { action: "initialize", feature, phase: requestedPhase || undefined });
5757
+ const snapshot = parseJsonResult(result);
5758
+ if (!snapshot?.ok) {
5759
+ ctx.ui.notify(String(snapshot?.reason ?? result.stderr), "error");
5827
5760
  return;
5828
5761
  }
5829
- const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
5830
- "--feature",
5831
- feature,
5832
- "--phase",
5833
- phase,
5834
- "--flow",
5835
- "feature-development",
5836
- "--write",
5837
- "--json",
5838
- ...(previousWorkflowComplete ? ["--force"] : []),
5839
- ]);
5840
- if (!controlPlaneSucceeded(state)) {
5841
- ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
5842
- return;
5843
- }
5844
- const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
5845
- "--write",
5846
- "--json",
5847
- ]);
5848
- if (!controlPlaneSucceeded(artifacts)) {
5849
- ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
5850
- return;
5851
- }
5852
- const inspected = await inspectWorkflow(ctx.cwd);
5853
- if (!inspected.result.ok || !inspected.snapshot) {
5854
- ctx.ui.notify(
5855
- `Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
5856
- "error",
5857
- );
5858
- return;
5859
- }
5860
- setControlState(ctx, planningState("requirement-draft"));
5861
- await pi.sendUserMessage(
5862
- [
5863
- "<devflow-requirement-draft-directive>",
5864
- `Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
5865
- "请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
5866
- "不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
5867
- "</devflow-requirement-draft-directive>",
5868
- ].join("\n"),
5869
- );
5762
+ await startDynamicTurn(ctx, snapshot);
5870
5763
  return;
5871
5764
  }
5872
5765
  ctx.ui.notify(