@guanlin4924/devflow-pi 0.1.106 → 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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Devflow Pi 是一个面向项目研发交付的独立 OMP 产品入口,内置
11
11
  安装与启动:
12
12
 
13
13
  ```bash
14
- npm install -g @guanlin4924/devflow-pi@0.1.106
14
+ npm install -g @guanlin4924/devflow-pi@0.1.107
15
15
  cd /path/to/your-project
16
16
  devflow-pi init
17
17
  devflow-pi doctor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.106",
3
+ "version": "0.1.107",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -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,
@@ -819,6 +820,7 @@ const REQUIRED_EXTENSION_TOOLS = [
819
820
  ];
820
821
 
821
822
  const REQUIRED_SKILLS = [
823
+ "workflow-dynamic",
822
824
  "code-index-query",
823
825
  "code-review",
824
826
  "requirement-feasibility-review",
@@ -876,6 +878,7 @@ const REQUIRED_MODEL_ROLES = ["@smol", "@task", "@slow"];
876
878
  const REQUIRED_AGENT_NAMES = new Set(Object.keys(REQUIRED_AGENTS));
877
879
 
878
880
  const REQUIRED_SCRIPTS = [
881
+ "workflow-orchestrator/scripts/dynamic_plan.rb",
879
882
  "workflow-orchestrator/scripts/init_workflow.rb",
880
883
  "workflow-orchestrator/scripts/init_state.rb",
881
884
  "workflow-orchestrator/scripts/phase_ownership.rb",
@@ -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));
@@ -2660,6 +2669,22 @@ export default function devflowCore(pi: ExtensionAPI) {
2660
2669
  return { result, snapshot: parseJsonResult(result) };
2661
2670
  }
2662
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
+
2663
2688
  async function inspectWorkflowWithRegistryRecovery(cwd: string): Promise<{ result: ProcessResult; snapshot: Record<string, unknown> | null }> {
2664
2689
  let inspected = await inspectWorkflow(cwd);
2665
2690
  const errors = Array.isArray(inspected.snapshot?.state_validation && (inspected.snapshot.state_validation as Record<string, unknown>).errors)
@@ -2819,6 +2844,10 @@ export default function devflowCore(pi: ExtensionAPI) {
2819
2844
  snapshot: Record<string, unknown>,
2820
2845
  options: { doctorAlreadyPassed: boolean },
2821
2846
  ): Promise<WorkflowResumeDisposition> {
2847
+ if (snapshot.mode === "dynamic") {
2848
+ await startDynamicTurn(ctx, snapshot);
2849
+ return snapshot.delivery === "ready" ? "complete" : "execute";
2850
+ }
2822
2851
  const disposition = workflowResumeDisposition(snapshot);
2823
2852
  const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
2824
2853
  if (disposition === "execute") {
@@ -3000,6 +3029,11 @@ export default function devflowCore(pi: ExtensionAPI) {
3000
3029
  ctx.ui.notify("当前步骤正在执行,等这一轮结束后再说「下一步」。", "info");
3001
3030
  return;
3002
3031
  }
3032
+ const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
3033
+ if (dynamic?.mode === "dynamic") {
3034
+ await startDynamicTurn(ctx, dynamic);
3035
+ return;
3036
+ }
3003
3037
  if (mode === "next") {
3004
3038
  const report = await runDoctor(ctx);
3005
3039
  if (!report.ok) {
@@ -3704,6 +3738,25 @@ export default function devflowCore(pi: ExtensionAPI) {
3704
3738
 
3705
3739
  pi.on("agent_end", async (event, ctx) => {
3706
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
+ }
3707
3760
  const assistant = [...event.messages].reverse().find(message => (
3708
3761
  message && typeof message === "object" && (message as { role?: unknown }).role === "assistant"
3709
3762
  ));
@@ -3750,6 +3803,17 @@ export default function devflowCore(pi: ExtensionAPI) {
3750
3803
 
3751
3804
  pi.on("input", async (event, ctx) => {
3752
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
+ }
3753
3817
  const controlAction = classifyNaturalControlAction(event.text);
3754
3818
  const stepNote = event.text.trim();
3755
3819
  if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/") && !classifyNextStepPhrase(stepNote)) {
@@ -3943,6 +4007,18 @@ export default function devflowCore(pi: ExtensionAPI) {
3943
4007
  });
3944
4008
 
3945
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
+ }
3946
4022
  const decision = await (async () => {
3947
4023
  const toolName = String(event.toolName).toLowerCase();
3948
4024
  const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
@@ -4108,6 +4184,18 @@ export default function devflowCore(pi: ExtensionAPI) {
4108
4184
  });
4109
4185
 
4110
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
+ }
4111
4199
  if (event.toolName === "task") {
4112
4200
  const binding = pendingReviewTasks.get(event.toolCallId);
4113
4201
  if (binding) {
@@ -5018,6 +5106,59 @@ export default function devflowCore(pi: ExtensionAPI) {
5018
5106
  },
5019
5107
  });
5020
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
+
5021
5162
  pi.registerTool({
5022
5163
  name: "devflow_bootstrap",
5023
5164
  label: "Devflow Bootstrap",
@@ -5025,11 +5166,24 @@ export default function devflowCore(pi: ExtensionAPI) {
5025
5166
  parameters: z.object({
5026
5167
  feature: z.string().min(1),
5027
5168
  phase: z.string().min(1).optional(),
5028
- 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"),
5029
5170
  force: z.boolean().default(false),
5030
5171
  }),
5031
5172
  approval: "write",
5032
- 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
+ }
5033
5187
  // Consume the one-shot runtime authorization before running Core's
5034
5188
  // atomic bootstrap sequence. A retry must obtain a fresh user approval.
5035
5189
  runtimeBootstrapAuthorizations.delete(String(_id));
@@ -5398,6 +5552,15 @@ export default function devflowCore(pi: ExtensionAPI) {
5398
5552
 
5399
5553
  const devflowCommand = async (args: string, ctx: DevflowCommandContext): Promise<void> => {
5400
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
+ }
5401
5564
  if (trimmed === "" || trimmed === "status") {
5402
5565
  await showWorkflowOverview(ctx);
5403
5566
  return;
@@ -5470,6 +5633,17 @@ export default function devflowCore(pi: ExtensionAPI) {
5470
5633
  if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
5471
5634
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
5472
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
+ }
5473
5647
  const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
5474
5648
  const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
5475
5649
  const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
@@ -5573,100 +5747,19 @@ export default function devflowCore(pi: ExtensionAPI) {
5573
5747
  }
5574
5748
  if (trimmed.startsWith("start ")) {
5575
5749
  const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
5576
- if (!feature) {
5577
- ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
5578
- return;
5579
- }
5580
- const phase = requestedPhase || defaultPhaseId(feature);
5581
- if (!validPhaseId(phase)) {
5582
- ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
5750
+ if (!feature || (requestedPhase && !validPhaseId(requestedPhase))) {
5751
+ ctx.ui.notify("用法:/devflow start <需求名>|<可选阶段标识>", "error");
5583
5752
  return;
5584
5753
  }
5585
5754
  const report = await runDoctor(ctx);
5586
- if (!report.ok) {
5587
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "info");
5588
-
5589
- }
5590
- const existing = await inspectWorkflow(ctx.cwd);
5591
- const existingFeature = existing.snapshot?.current_feature;
5592
- const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
5593
- if (
5594
- !previousWorkflowComplete &&
5595
- typeof existingFeature === "string" &&
5596
- existingFeature !== feature
5597
- ) {
5598
- ctx.ui.notify(
5599
- `当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
5600
- "error",
5601
- );
5602
- return;
5603
- }
5604
- // This dry-run happens before init_workflow.rb, so a rejected feature
5605
- // can never overwrite generated workflow files belonging to an active
5606
- // owner or a legacy phase directory that has no trustworthy owner.
5607
- const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
5608
- "claim",
5609
- "--feature",
5610
- feature,
5611
- "--phase",
5612
- phase,
5613
- "--flow",
5614
- "feature-development",
5615
- "--json",
5616
- ]);
5617
- if (!controlPlaneSucceeded(ownership)) {
5618
- ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "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");
5619
5760
  return;
5620
5761
  }
5621
- const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
5622
- "--write",
5623
- "--json",
5624
- ]);
5625
- if (!controlPlaneSucceeded(workflow)) {
5626
- ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
5627
- return;
5628
- }
5629
- const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
5630
- "--feature",
5631
- feature,
5632
- "--phase",
5633
- phase,
5634
- "--flow",
5635
- "feature-development",
5636
- "--write",
5637
- "--json",
5638
- ...(previousWorkflowComplete ? ["--force"] : []),
5639
- ]);
5640
- if (!controlPlaneSucceeded(state)) {
5641
- ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
5642
- return;
5643
- }
5644
- const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
5645
- "--write",
5646
- "--json",
5647
- ]);
5648
- if (!controlPlaneSucceeded(artifacts)) {
5649
- ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
5650
- return;
5651
- }
5652
- const inspected = await inspectWorkflow(ctx.cwd);
5653
- if (!inspected.result.ok || !inspected.snapshot) {
5654
- ctx.ui.notify(
5655
- `Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
5656
- "error",
5657
- );
5658
- return;
5659
- }
5660
- setControlState(ctx, planningState("requirement-draft"));
5661
- await pi.sendUserMessage(
5662
- [
5663
- "<devflow-requirement-draft-directive>",
5664
- `Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
5665
- "请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
5666
- "不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
5667
- "</devflow-requirement-draft-directive>",
5668
- ].join("\n"),
5669
- );
5762
+ await startDynamicTurn(ctx, snapshot);
5670
5763
  return;
5671
5764
  }
5672
5765
  ctx.ui.notify(
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: workflow-dynamic
3
+ description: Execute a Core-owned dynamic requirement plan, maintain one readable requirement document, route technical findings to repair skills, and ask only genuine business decisions. Use when the inspector reports mode dynamic; existing fixed workflows keep their own contracts.
4
+ ---
5
+
6
+ # Dynamic requirement delivery
7
+
8
+ Use `devflow_plan inspect` as the scheduling authority. Present task titles and progress in the user's language. Do not edit STATE, synthesize test results, or use legacy complete-step/checkpoint tools.
9
+
10
+ ## Intent and planning
11
+
12
+ Read the single document returned by Core and inspect relevant source. Update that document for scope, approach, and acceptance requirements. Each acceptance clause has a stable heading such as `### R-1: Do not send another batch after the deadline`. Preserve IDs across wording changes. Cosmetic whitespace does not change the obligation. Removing an obligation requires a genuine scope decision; never delete a failed obligation to finish.
13
+
14
+ Propose tasks with `revise`, including its inspected `expected_revision` and a plain-language change reason. Fields are `id`, `title`, `kind`, `requirements`, `dependencies`, `inputs`, `outputs`, `checks`. Allowed kinds: discover, implement, verify, review. Each check declares an executable `argv` array and optional timeout seconds. Inputs must include the relevant production code, tests, and configuration: a test file alone is insufficient. Prefer directories where additional source files affect behavior. Every acceptance clause needs a verification task. Code changes need an independent review task that depends on the implementation and verification tasks.
15
+
16
+ Generate only applicable work. A backend-only feature has no UI, wireframe, browser-test, or frontend-document task. If applicability is unclear, investigate it before proposing that branch. Do not generate no-op documents. Tasks may produce source, tests, migrations, schemas or genuinely needed design assets; their existence must have a consumer.
17
+
18
+ ## Execution
19
+
20
+ 1. Inspect and claim the task selected by Core. Use the returned task ID and attempt for all subsequent results.
21
+ 2. Read its exact Skill ID. In dynamic mode use that skill's technical procedures, but the dynamic task supplies scope, inputs, outputs and acceptance. Legacy stage lists, mandatory Markdown/meta pairs and checkpoint instructions do not apply.
22
+ 3. For discovery/implementation/repair, do the work and call `report` with a factual summary. This records activity completion, not an acceptance pass.
23
+ 4. For verification, call `verify`. Core executes the declared commands and captures exit status and output. Do not substitute a shell transcript or self-reported pass. Use tests that assert the actual expected behavior, including production paths and meaningful failure scenarios.
24
+ 5. For review, dispatch a read-only `devflow-code-reviewer` task with the requirement document, task, source paths and recorded verification evidence. Ask for strict JSON `{"summary":"...","findings":[...]}`; each finding identifies the violated requirement, evidence, affected scope and suggested recheck. Submit its task-call ID using `review_result`; Core accepts only the captured child result, not your rewritten report.
25
+ 6. Continue ready tasks. Failed checks automatically create a bounded repair task. Read the finding and invoke its repair skill, then allow Core to rerun the original verification. Do not reset repair budgets by renaming tasks.
26
+
27
+ ## Changes, waiting, and recovery
28
+
29
+ Keep stable task IDs. To cancel unnecessary work, retain the task with `cancelled: true` and a reason and rewire its consumers in the same proposal. Preserve verification coverage. A failed required behavior is a repair, not an irrelevant task.
30
+
31
+ After a requirement edit, inspect and revise only affected work. Old evidence remains history and cannot certify changed inputs. Never weaken checks merely to obtain a pass.
32
+
33
+ Use `decision` only for business alternatives, scope reduction, residual-risk choices, or actions outside existing authorization. Supply one clear question and two or three options with consequences; place the recommendation first. Core displays and records the actual user choice. Missing metadata, stale indexes, missing tests and repairable code defects are technical work, not user decisions. A cancelled choice stays pending; it is not approval. Continue independent tasks if possible.
34
+
35
+ For removed acceptance clauses, include their IDs in `remove_requirements` and identify the exact `approval_option` in the decision. Only that actual user selection authorizes removing those clauses; bind the subsequent revision with `scope_decision_id`. A generic confirmation or a choice to retain scope is not permission to remove acceptance requirements.
36
+
37
+ For an interrupted running/checking task, inspect actual effects first. Use `recover` with the findings only after establishing the previous operation is no longer running and retry is appropriate. Do not rerun uncertain external writes. Repair exhaustion remains failed: explain what was attempted and actionable next choices without claiming delivery complete.
38
+
39
+ If an external dependency is unavailable, use `wait` with the task attempt and concrete reason. After read-only evidence establishes readiness, use `resume`; this creates eligibility for a new attempt. Do not classify missing generated documents as external dependencies.
40
+
41
+ Summarize the readable task board and remaining decisions. Detailed machine evidence stays in Core; do not generate another report document for each invocation.
@@ -3,6 +3,9 @@ name: workflow-orchestrator
3
3
  description: Use when the user asks to continue, resume, advance, inspect, or route a .devflow/planning-based workflow; when the OMP development agent must decide the next workflow skill from project state; or when a checkpoint must be handled before proceeding.
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  ## 默认软门禁策略
7
10
 
8
11
  工作流硬门禁已移除。下文及各步骤中的产物、索引、合同、审查、质量、恢复和 CP 审批条件均为诊断建议:保留失败证据和缺项,允许继续执行。用户说“下一步”即可推进,无需额外批准弹窗;不得把继续执行记录成人工批准,也不得把失败、未执行的测试改写为通过。真实读写/执行失败、无法解析的状态和并发版本冲突须如实报告。硬门禁及其恢复开关已移除。
@@ -15,6 +15,12 @@ require_relative "validate_state"
15
15
  require_relative "phase_ownership"
16
16
  require_relative "requirement_graph"
17
17
  require_relative "gate_runtime"
18
+ require_relative "dynamic_plan"
19
+
20
+ if DynamicPlan.read_state(Dir.pwd)&.dig("current_flow") == "dynamic"
21
+ puts JSON.generate({ "ok" => false, "reason" => "dynamic_plan_requires_evidence", "message" => "Use devflow_plan; legacy complete-step cannot complete dynamic tasks." })
22
+ exit 1
23
+ end
18
24
 
19
25
  options = {
20
26
  state: nil,
@@ -0,0 +1,504 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "digest"
6
+ require "securerandom"
7
+ require "open3"
8
+ require "timeout"
9
+ require_relative "control_io"
10
+
11
+ # STATE is the only mutable authority. Documents own intent, never verdicts.
12
+ module DynamicPlan
13
+ SKILLS = {
14
+ "discover" => "workflow-requirement-triage",
15
+ "implement" => "workflow-execute-phase",
16
+ "repair" => "workflow-bug-fix",
17
+ "verify" => "workflow-run-acceptance-tests",
18
+ "review" => "code-review"
19
+ }.freeze
20
+ module_function
21
+
22
+ def digest(value)
23
+ Digest::SHA256.hexdigest(JSON.generate(value))
24
+ end
25
+
26
+ def normalize(text)
27
+ text.to_s.split.join(" ")
28
+ end
29
+
30
+ def path(root, relative)
31
+ raise ArgumentError, "project-relative path required" unless relative.is_a?(String) && !relative.empty? && !relative.start_with?("/") && !relative.split(/[\\\/]/).include?("..")
32
+ absolute = File.expand_path(relative, root)
33
+ cursor = absolute
34
+ cursor = File.dirname(cursor) until File.exist?(cursor) || File.symlink?(cursor)
35
+ real = File.realpath(cursor)
36
+ raise ArgumentError, "path escapes project" unless real == File.realpath(root) || real.start_with?(File.realpath(root) + "/")
37
+ absolute
38
+ end
39
+
40
+ def clauses(root, plan)
41
+ text = File.read(path(root, plan.fetch("document")))
42
+ result = {}
43
+ text.scan(/^### (R-[A-Za-z0-9_-]+)[::]([^\n]*\n(?:(?!^### |^## ).*\n?)*)/).each do |id, body|
44
+ raise ArgumentError, "duplicate requirement #{id}" if result.key?(id)
45
+ result[id] = normalize(body)
46
+ end
47
+ result
48
+ end
49
+
50
+ def file_pin(root, relative)
51
+ absolute = path(root, relative)
52
+ return "missing" unless File.exist?(absolute)
53
+ return Digest::SHA256.file(absolute).hexdigest if File.file?(absolute)
54
+ entries = Dir.glob(File.join(absolute, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |entry|
55
+ next if entry.sub(absolute + "/", "").split("/").any? { |part| %w[.git node_modules .devflow __pycache__].include?(part) }
56
+ next unless File.file?(entry)
57
+ safe = path(root, entry.sub(File.expand_path(root) + "/", ""))
58
+ [entry.sub(absolute + "/", ""), Digest::SHA256.file(safe).hexdigest]
59
+ end
60
+ digest(entries)
61
+ end
62
+
63
+ def pins(root, plan, task)
64
+ requirements = clauses(root, plan)
65
+ {
66
+ "requirements" => task.fetch("requirements").to_h { |id| [id, requirements[id]] },
67
+ "inputs" => task.fetch("inputs").to_h { |file| [file, file_pin(root, file)] },
68
+ "spec" => digest(task.slice("kind", "requirements", "inputs", "outputs", "checks"))
69
+ }
70
+ end
71
+
72
+ def validate!(root, plan)
73
+ tasks = plan.fetch("tasks")
74
+ raise ArgumentError, "tasks must be an array" unless tasks.is_a?(Array)
75
+ ids = tasks.map { |task| task.fetch("id") }
76
+ raise ArgumentError, "duplicate task ids" unless ids.uniq == ids
77
+ tasks.each do |task|
78
+ raise ArgumentError, "invalid task id" unless task["id"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}\z/)
79
+ raise ArgumentError, "unknown capability" unless SKILLS.key?(task["kind"])
80
+ raise ArgumentError, "task title required" if task["title"].to_s.strip.empty?
81
+ %w[dependencies requirements inputs outputs checks].each { |key| raise ArgumentError, "#{key} must be an array" unless task[key].is_a?(Array) }
82
+ raise ArgumentError, "unknown dependency" unless (task["dependencies"] - ids).empty?
83
+ (task["inputs"] + task["outputs"]).each { |file| path(root, file) }
84
+ raise ArgumentError, "verification needs inputs and executable checks" if task["kind"] == "verify" && (task["checks"].empty? || task["inputs"].empty?)
85
+ raise ArgumentError, "review needs source inputs" if task["kind"] == "review" && task["inputs"].empty?
86
+ task["checks"].each do |check|
87
+ argv = check["argv"]
88
+ raise ArgumentError, "checks require nonempty argv arrays" unless argv.is_a?(Array) && argv.any? && argv.all? { |arg| arg.is_a?(String) && !arg.include?("\0") }
89
+ end
90
+ next unless task["status"] == "cancelled"
91
+ raise ArgumentError, "cancellation reason required" if task["reason"].to_s.strip.empty?
92
+ raise ArgumentError, "cancelled task still has consumers" if tasks.any? { |other| other["status"] != "cancelled" && other["dependencies"].include?(task["id"]) }
93
+ end
94
+ visited, visiting = [], []
95
+ visit = lambda do |id|
96
+ raise ArgumentError, "cyclic task dependencies" if visiting.include?(id)
97
+ return if visited.include?(id)
98
+ visiting << id
99
+ tasks.find { |task| task["id"] == id }["dependencies"].each { |dependency| visit.call(dependency) }
100
+ visiting.delete(id)
101
+ visited << id
102
+ end
103
+ ids.each { |id| visit.call(id) }
104
+ requirements = clauses(root, plan)
105
+ active = tasks.reject { |task| task["status"] == "cancelled" }
106
+ active.each do |task|
107
+ raise ArgumentError, "unknown requirement" unless (task["requirements"] - requirements.keys).empty?
108
+ end
109
+ unless tasks.empty? || plan["cancelled"]
110
+ raise ArgumentError, "requirement clauses missing from main document" if requirements.empty?
111
+ requirements.each_key do |id|
112
+ raise ArgumentError, "#{id} has no verification task" unless active.any? { |task| task["kind"] == "verify" && task["requirements"].include?(id) }
113
+ end
114
+ if active.any? { |task| %w[implement repair].include?(task["kind"]) }
115
+ raise ArgumentError, "code changes require independent review" unless active.any? { |task| task["kind"] == "review" }
116
+ end
117
+ ancestors = lambda do |task|
118
+ task["dependencies"].flat_map do |id|
119
+ [id, *ancestors.call(tasks.find { |other| other["id"] == id })]
120
+ end.uniq
121
+ end
122
+ active.select { |task| task["kind"] == "verify" }.each do |task|
123
+ producers = active.select { |other| other["kind"] == "implement" && (other["requirements"] & task["requirements"]).any? }
124
+ raise ArgumentError, "verification must depend on its implementation" unless (producers.map { |item| item["id"] } - ancestors.call(task)).empty?
125
+ end
126
+ if active.any? { |task| %w[implement repair].include?(task["kind"]) }
127
+ changed_requirements = active.select { |task| %w[implement repair].include?(task["kind"]) }.flat_map { |task| task["requirements"] }.uniq
128
+ changed_requirements.each do |id|
129
+ verifiers = active.select { |task| task["kind"] == "verify" && task["requirements"].include?(id) }
130
+ covered = active.any? do |task|
131
+ task["kind"] == "review" && task["requirements"].include?(id) && (verifiers.map { |item| item["id"] } - ancestors.call(task)).empty?
132
+ end
133
+ raise ArgumentError, "#{id} needs review after verification" unless covered
134
+ end
135
+ end
136
+ end
137
+ true
138
+ end
139
+
140
+ def refresh!(root, plan)
141
+ plan["tasks"].each do |task|
142
+ next unless task["status"] == "completed" && task["evidence"]
143
+ current_pins = pins(root, plan, task)
144
+ observed_pins = task["evidence"]["pins"]
145
+ unchanged = if %w[implement repair].include?(task["kind"])
146
+ observed_pins.slice("requirements", "spec") == current_pins.slice("requirements", "spec")
147
+ else
148
+ observed_pins == current_pins
149
+ end
150
+ next if unchanged
151
+ task["status"] = "pending"
152
+ task["verification"] = "stale"
153
+ end
154
+ loop do
155
+ changed = false
156
+ plan["tasks"].each do |task|
157
+ next unless task["status"] == "completed"
158
+ next if task["dependencies"].all? { |id| plan["tasks"].find { |other| other["id"] == id }&.dig("status") == "completed" }
159
+ task["status"], task["verification"], changed = "pending", "stale", true
160
+ end
161
+ break unless changed
162
+ end
163
+ end
164
+
165
+ def snapshot(root, state)
166
+ plan = state.fetch("plan")
167
+ refresh!(root, plan)
168
+ validation_error = begin
169
+ validate!(root, plan)
170
+ nil
171
+ rescue ArgumentError, KeyError => error
172
+ error.message
173
+ end
174
+ tasks = plan["tasks"]
175
+ ready = tasks.find do |task|
176
+ task["status"] == "pending" && task["dependencies"].all? { |id| tasks.find { |other| other["id"] == id }&.dig("status") == "completed" }
177
+ end
178
+ running = tasks.find { |task| %w[running checking].include?(task["status"]) }
179
+ complete = tasks.any? && tasks.all? { |task| %w[completed cancelled].include?(task["status"]) } && plan.fetch("decisions", []).none? { |item| item["status"] == "pending" }
180
+ action = if plan["cancelled"] then "workflow_cancelled"
181
+ elsif validation_error then "reconcile_plan"
182
+ elsif tasks.empty? then "plan_work"
183
+ elsif running then "resume_task"
184
+ elsif ready then "run_task"
185
+ elsif complete then "workflow_complete"
186
+ else "waiting" end
187
+ selected = running || ready
188
+ {
189
+ "ok" => true, "mode" => "dynamic", "action" => action, "revision" => state["revision"],
190
+ "current_flow" => "dynamic", "current_feature" => state["current_feature"], "current_phase" => state["current_phase"],
191
+ "status" => plan["cancelled"] ? "cancelled" : (complete && !validation_error ? "completed" : "in_progress"),
192
+ "document" => plan["document"], "plan_revision" => plan["revision"], "reason" => validation_error,
193
+ "progress_key" => digest(tasks.map { |task| task.slice("id", "status", "verification", "attempt").merge("results" => task.fetch("history", []).length) }),
194
+ "delivery" => plan["cancelled"] ? "cancelled" : (complete && !validation_error ? "ready" : "incomplete"),
195
+ "current_step" => selected && selected.merge("label" => selected["title"], "skill" => SKILLS[selected["kind"]]),
196
+ "workflow_steps" => tasks.map { |task| task.slice("id", "status", "verification", "reason").merge("label" => task["title"]) },
197
+ "tasks" => tasks, "decisions" => plan.fetch("decisions", [])
198
+ }
199
+ end
200
+
201
+ def read_state(root)
202
+ file, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/STATE.md", :state)
203
+ return nil unless File.file?(file)
204
+ YAML.safe_load(File.read(file)[/\A---\n(.*?)\n---\n/m, 1], permitted_classes: [], aliases: false)
205
+ end
206
+
207
+ def save(root, state)
208
+ state["revision"] += 1
209
+ view = snapshot(root, state)
210
+ state["status"] = view["status"]
211
+ state["current_step"] = view.dig("current_step", "id")
212
+ state["completed_steps"] = state["plan"]["tasks"].select { |task| task["status"] == "completed" }.map { |task| task["id"] }
213
+ state["pending_steps"] = state["plan"]["tasks"].reject { |task| %w[completed cancelled].include?(task["status"]) }.map { |task| task["id"] }
214
+ file, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/STATE.md", :state)
215
+ # JSON round-trip removes shared object identities; safe YAML readers must
216
+ # never need aliases to read a runtime state snapshot.
217
+ DevflowControlIO.atomic_write(file, JSON.parse(JSON.generate(state)).to_yaml + "---\n\n# Runtime state\n\nManaged by Core. Edit the requirement document to change intent.\n")
218
+ snapshot(root, state)
219
+ end
220
+
221
+ def transact(root, expected)
222
+ lock = DevflowControlIO.acquire_project_lock(root)
223
+ state = read_state(root)
224
+ raise ArgumentError, "dynamic plan not initialized" unless state && state["current_flow"] == "dynamic"
225
+ raise ArgumentError, "REVISION_CONFLICT: inspect again" unless expected.is_a?(Integer) && expected == state["revision"]
226
+ before = digest(state)
227
+ refresh!(root, state["plan"])
228
+ yield state
229
+ return snapshot(root, state) if digest(state) == before
230
+ save(root, state)
231
+ ensure
232
+ DevflowControlIO.release_lock(lock)
233
+ end
234
+
235
+ def run_check(root, check)
236
+ output, status, timed_out = +"", nil, false
237
+ seconds = [[Integer(check.fetch("timeout", 120)), 1].max, 600].min
238
+ Open3.popen2e(*check.fetch("argv"), chdir: root, pgroup: true) do |stdin, stream, wait|
239
+ stdin.close
240
+ reader = Thread.new do
241
+ loop do
242
+ chunk = stream.readpartial(4096)
243
+ output << chunk if output.bytesize < 32_768
244
+ end
245
+ rescue EOFError, IOError
246
+ nil
247
+ end
248
+ begin
249
+ Timeout.timeout(seconds) { status = wait.value.exitstatus }
250
+ rescue Timeout::Error
251
+ timed_out = true
252
+ Process.kill("KILL", -wait.pid) rescue nil
253
+ wait.value
254
+ ensure
255
+ # Drain the exited child's buffered output before closing the pipe.
256
+ # A descendant retaining the pipe cannot keep recovery waiting forever.
257
+ reader.join(1)
258
+ stream.close unless stream.closed?
259
+ reader.join
260
+ end
261
+ end
262
+ { "argv" => check["argv"], "exit_code" => status, "timeout" => timed_out, "output" => output.encode("UTF-8", invalid: :replace, undef: :replace), "passed" => status == 0 && !timed_out }
263
+ rescue SystemCallError => error
264
+ { "argv" => check["argv"], "passed" => false, "output" => error.message }
265
+ end
266
+
267
+ def failure!(plan, task, evidence)
268
+ task["status"], task["verification"] = "failed", "fail"
269
+ fingerprint = digest(evidence["checks"]&.map { |check| check.slice("argv", "exit_code", "output") } || evidence["review"])
270
+ previous = task.fetch("history", [])[0...-1].last
271
+ attempts = task.fetch("repair_attempts", 0)
272
+ if attempts >= 2 || (previous && previous["finding"] == fingerprint && previous["pins"] == evidence["pins"])
273
+ task["reason"] = "自动修复已停止:达到次数上限或没有进展;保留失败结果。"
274
+ return
275
+ end
276
+ evidence["finding"] = fingerprint
277
+ task["repair_attempts"] = attempts + 1
278
+ repair_id = "repair-#{digest(task['id'])[0, 12]}-#{attempts + 1}"
279
+ task["status"] = "pending"
280
+ task["dependencies"] << repair_id
281
+ plan["tasks"] << {
282
+ "id" => repair_id, "title" => "修复:#{task['title']}", "kind" => "repair", "requirements" => task["requirements"].dup,
283
+ "inputs" => task["inputs"].dup, "outputs" => [], "checks" => [], "dependencies" => [], "status" => "pending",
284
+ "verification" => "not_run", "finding" => evidence, "repairs" => task["id"], "history" => []
285
+ }
286
+ unless plan["tasks"].any? { |item| item["kind"] == "review" && item["status"] != "cancelled" && (task["requirements"] - item["requirements"]).empty? }
287
+ plan["tasks"] << {
288
+ "id" => "review-#{digest(task['id'])[0, 12]}-#{attempts + 1}", "title" => "独立审查修复结果", "kind" => "review", "requirements" => task["requirements"].dup,
289
+ "inputs" => task["inputs"].dup, "outputs" => [], "checks" => [], "dependencies" => plan["tasks"].select { |item| item["kind"] == "verify" && item["status"] != "cancelled" && (item["requirements"] & task["requirements"]).any? }.map { |item| item["id"] },
290
+ "status" => "pending", "verification" => "not_run", "history" => []
291
+ }
292
+ end
293
+ plan["revision"] += 1
294
+ end
295
+
296
+ def dispatch(root, request)
297
+ action = request.fetch("action")
298
+ if action == "inspect"
299
+ state = read_state(root)
300
+ return { "ok" => true, "mode" => "legacy" } unless state && state["current_flow"] == "dynamic"
301
+ return snapshot(root, state)
302
+ end
303
+ if action == "initialize"
304
+ lock = DevflowControlIO.acquire_project_lock(root)
305
+ begin
306
+ old = read_state(root)
307
+ raise ArgumentError, "active workflow exists; continue it or explicitly migrate" if old && !%w[completed cancelled].include?(old["status"])
308
+ feature = request.fetch("feature").to_s.strip
309
+ raise ArgumentError, "feature required" if feature.empty?
310
+ if old
311
+ archive = path(root, ".devflow/runs/plans/#{Digest::SHA256.hexdigest(old['current_phase'].to_s)[0, 16]}-#{SecureRandom.uuid}.json")
312
+ DevflowControlIO.atomic_write(archive, JSON.pretty_generate(old))
313
+ end
314
+ phase = request["phase"] || "feature-#{Digest::SHA256.hexdigest(feature)[0, 12]}-#{SecureRandom.hex(3)}"
315
+ raise ArgumentError, "invalid phase identifier" unless phase.is_a?(String) && phase.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\z/)
316
+ document = ".devflow/planning/features/#{phase}/需求.md"
317
+ absolute = path(root, document)
318
+ raise ArgumentError, "requirement document already exists; choose a new phase or resume" if File.exist?(absolute)
319
+ DevflowControlIO.atomic_write(absolute, "# #{feature}\n\n## 目标与范围\n\n#{request.fetch('description', feature)}\n\n## 验收要求\n\n在需求核实后,将可验证要求写成 `### R-1: 要求描述`,每条保留稳定编号。\n\n## 方案与决定\n\n执行进度由 Core 展示;此处不手工记录通过状态。\n")
320
+ state = { "state_schema_version" => 3, "revision" => old ? old["revision"] : -1, "current_flow" => "dynamic", "current_feature" => feature, "current_phase" => phase,
321
+ "plan" => { "version" => 1, "revision" => 0, "document" => document, "tasks" => [], "decisions" => [], "changes" => [] } }
322
+ return save(root, state)
323
+ ensure
324
+ DevflowControlIO.release_lock(lock)
325
+ end
326
+ end
327
+ if action == "verify"
328
+ claim = transact(root, request["expected_revision"]) do |state|
329
+ task = state["plan"]["tasks"].find { |item| item["id"] == request["task_id"] }
330
+ raise ArgumentError, "running verification attempt required" unless task && task["kind"] == "verify" && task["status"] == "running" && task["attempt"] == request["attempt"]
331
+ task["status"] = "checking"
332
+ task["runner_pid"] = Process.pid
333
+ task["check_pins"] = pins(root, state["plan"], task)
334
+ end
335
+ task = claim["tasks"].find { |item| item["id"] == request["task_id"] }
336
+ checks = task["checks"].map { |check| run_check(root, check) }
337
+ return transact(root, claim["revision"]) do |state|
338
+ current = state["plan"]["tasks"].find { |item| item["id"] == task["id"] }
339
+ raise ArgumentError, "verification inputs changed during execution" unless current["attempt"] == task["attempt"] && task["check_pins"] == pins(root, state["plan"], current)
340
+ evidence = { "attempt" => task["attempt"], "pins" => task["check_pins"], "checks" => checks, "at" => Time.now.utc.iso8601 }
341
+ current["history"] << evidence
342
+ current["evidence"] = evidence
343
+ if checks.all? { |check| check["passed"] }
344
+ current["status"], current["verification"] = "completed", "pass"
345
+ else
346
+ failure!(state["plan"], current, evidence)
347
+ end
348
+ end
349
+ end
350
+ transact(root, request["expected_revision"]) do |state|
351
+ plan = state["plan"]
352
+ raise ArgumentError, "workflow was cancelled; create a new requirement" if plan["cancelled"]
353
+ case action
354
+ when "cancel"
355
+ raise ArgumentError, "cancellation must come from user runtime action" unless request["runtime_cancel"] == true
356
+ plan["cancelled"] = true
357
+ plan["cancel_reason"] = request.fetch("reason")
358
+ plan["tasks"].each do |task|
359
+ next if task["status"] == "completed"
360
+ task["status"], task["reason"] = "cancelled", plan["cancel_reason"]
361
+ task.delete("attempt")
362
+ end
363
+ when "revise"
364
+ old_tasks = plan["tasks"].to_h { |task| [task["id"], task] }
365
+ new_requirements = clauses(root, plan)
366
+ scope_decision = plan["decisions"].find { |item| item["id"] == request["scope_decision_id"] }
367
+ permitted_removals = if scope_decision && scope_decision["status"] == "answered" && scope_decision["choice"] == scope_decision["approval_option"] && scope_decision["requirements_pin"] == digest(plan.fetch("requirements", {}))
368
+ scope_decision.fetch("remove_requirements", [])
369
+ else [] end
370
+ proposal = request.fetch("tasks")
371
+ raise ArgumentError, "keep removed tasks as cancelled with reasons" unless (old_tasks.keys - proposal.map { |task| task["id"] }).empty?
372
+ new_tasks = proposal.map do |item|
373
+ spec = item.slice("id", "title", "kind", "requirements", "inputs", "outputs", "checks", "dependencies")
374
+ %w[requirements inputs outputs checks dependencies].each { |key| spec[key] ||= [] }
375
+ old = old_tasks[item["id"]]
376
+ candidate = (old || { "status" => "pending", "verification" => "not_run", "history" => [] }).merge(spec)
377
+ changed = old && spec.any? { |key, value| key != "title" && old[key] != value }
378
+ if changed
379
+ candidate["status"], candidate["verification"] = "pending", "stale"
380
+ candidate.delete("attempt")
381
+ end
382
+ if item["cancelled"] == true
383
+ if old && old["verification"] == "fail"
384
+ withdrawn = (old["requirements"] - permitted_removals).empty? && (old["requirements"] & new_requirements.keys).empty?
385
+ raise ArgumentError, "cannot cancel a failing obligation without its explicit scope withdrawal" unless withdrawn
386
+ end
387
+ candidate["status"], candidate["reason"] = "cancelled", item["reason"]
388
+ candidate.delete("attempt")
389
+ end
390
+ candidate
391
+ end
392
+ plan["tasks"] = new_tasks
393
+ validate!(root, plan)
394
+ old_requirements = plan.fetch("requirements", {})
395
+ removed = old_requirements.keys - new_requirements.keys
396
+ raise ArgumentError, "removing acceptance requirements needs an explicit scope decision for those clauses" unless (removed - permitted_removals).empty?
397
+ plan["requirements"] = new_requirements
398
+ plan["revision"] += 1
399
+ plan["changes"] << { "revision" => plan["revision"], "reason" => request.fetch("reason"), "at" => Time.now.utc.iso8601 }
400
+ when "claim"
401
+ view = snapshot(root, state)
402
+ raise ArgumentError, "no runnable task" unless view["action"] == "run_task"
403
+ task = plan["tasks"].find { |item| item["id"] == view.dig("current_step", "id") }
404
+ task["status"], task["attempt"] = "running", SecureRandom.uuid
405
+ task["started_at"] = Time.now.utc.iso8601
406
+ task["dispatch_pins"] = pins(root, plan, task)
407
+ when "report", "review_result"
408
+ task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
409
+ raise ArgumentError, "stale or missing task attempt" unless task && task["status"] == "running" && task["attempt"] == request["attempt"]
410
+ raise ArgumentError, "verification must run through Core" if task["kind"] == "verify"
411
+ if task["kind"] == "review"
412
+ raise ArgumentError, "independent reviewer result required" unless action == "review_result" && request["runtime_review"] == true
413
+ raise ArgumentError, "review inputs changed" unless task["dispatch_pins"] == pins(root, plan, task)
414
+ report = request.fetch("report")
415
+ raise ArgumentError, "invalid reviewer report" unless report["findings"].is_a?(Array) && report["summary"].is_a?(String)
416
+ evidence = { "pins" => pins(root, plan, task), "review" => report, "task_call_id" => request.fetch("task_call_id"), "attempt" => task["attempt"] }
417
+ task["history"] << evidence
418
+ task["evidence"] = evidence
419
+ if report["findings"].empty?
420
+ task["status"], task["verification"] = "completed", "pass"
421
+ else
422
+ failure!(plan, task, evidence)
423
+ end
424
+ else
425
+ raise ArgumentError, "requirements changed during execution" unless task["dispatch_pins"]["requirements"] == pins(root, plan, task)["requirements"]
426
+ raise ArgumentError, "result summary required" if request["summary"].to_s.strip.empty?
427
+ raise ArgumentError, "expected output missing" unless task["outputs"].all? { |file| File.exist?(path(root, file)) }
428
+ # Implementation is an activity result, never an acceptance pass.
429
+ evidence = { "pins" => pins(root, plan, task), "summary" => request["summary"], "attempt" => task["attempt"] }
430
+ task["evidence"] = evidence
431
+ task["history"] << evidence
432
+ task["status"], task["verification"] = "completed", "not_run"
433
+ end
434
+ when "recover"
435
+ task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
436
+ raise ArgumentError, "only an interrupted attempt may recover" unless task && %w[running checking].include?(task["status"])
437
+ if task["status"] == "checking" && task["runner_pid"]
438
+ alive = begin
439
+ Process.kill(0, task["runner_pid"])
440
+ true
441
+ rescue Errno::ESRCH
442
+ false
443
+ end
444
+ raise ArgumentError, "verification process is still running" if alive
445
+ end
446
+ raise ArgumentError, "inspect actual effects and provide a recovery reason" if request["reason"].to_s.strip.empty?
447
+ task["history"] << { "attempt" => task["attempt"], "interrupted" => true, "reason" => request["reason"] }
448
+ task["status"] = "pending"
449
+ task.delete("attempt")
450
+ when "wait"
451
+ task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
452
+ raise ArgumentError, "running attempt required" unless task && task["status"] == "running" && task["attempt"] == request["attempt"]
453
+ raise ArgumentError, "external dependency reason required" if request["reason"].to_s.strip.empty?
454
+ task["status"], task["reason"] = "waiting_external", request["reason"]
455
+ when "resume"
456
+ task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
457
+ raise ArgumentError, "external waiting task required" unless task && task["status"] == "waiting_external"
458
+ raise ArgumentError, "evidence that external dependency is ready required" if request["reason"].to_s.strip.empty?
459
+ task["history"] << { "resumed" => true, "reason" => request["reason"] }
460
+ task["status"] = "pending"
461
+ task.delete("attempt")
462
+ when "decision"
463
+ options = request.fetch("options")
464
+ raise ArgumentError, "provide two or three concrete options" unless options.is_a?(Array) && (2..3).include?(options.length) && options.all? { |item| item.is_a?(String) && !item.strip.empty? }
465
+ question = request.fetch("question")
466
+ removals = request.fetch("remove_requirements", [])
467
+ raise ArgumentError, "scope removal must name existing requirement IDs" unless removals.is_a?(Array) && (removals - plan.fetch("requirements", {}).keys).empty?
468
+ raise ArgumentError, "scope removal needs an explicit approving option" if removals.any? && !options.include?(request["approval_option"])
469
+ binding = file_pin(root, plan["document"])
470
+ unless plan["decisions"].any? { |item| item["question"] == question && item["document_pin"] == binding }
471
+ plan["decisions"].select { |item| item["question"] == question && item["status"] == "pending" }.each { |item| item["status"] = "superseded" }
472
+ affected = request["task_ids"] || [snapshot(root, state).dig("current_step", "id")].compact
473
+ raise ArgumentError, "unknown affected decision task" unless (affected - plan["tasks"].map { |task| task["id"] }).empty?
474
+ blocked = plan["tasks"].select { |task| affected.include?(task["id"]) && !%w[completed cancelled].include?(task["status"]) }
475
+ prior = blocked.to_h { |task| [task["id"], task["status"] == "waiting_decision" ? "pending" : task["status"]] }
476
+ blocked.each { |task| task["status"] = "waiting_decision" }
477
+ plan["decisions"] << { "id" => SecureRandom.uuid, "question" => question, "options" => options, "status" => "pending", "document_pin" => binding, "requirements_pin" => digest(plan.fetch("requirements", {})), "blocked_tasks" => prior, "remove_requirements" => removals, "approval_option" => request["approval_option"] }
478
+ end
479
+ when "decide"
480
+ raise ArgumentError, "user response must come from runtime UI" unless request["runtime_decision"] == true
481
+ decision = plan["decisions"].find { |item| item["id"] == request["decision_id"] }
482
+ raise ArgumentError, "decision expired" unless decision && decision["status"] == "pending" && decision["document_pin"] == file_pin(root, plan["document"])
483
+ raise ArgumentError, "invalid option" unless decision["options"].include?(request["choice"])
484
+ decision["status"], decision["choice"] = "answered", request["choice"]
485
+ decision.fetch("blocked_tasks", {}).each do |id, prior|
486
+ task = plan["tasks"].find { |item| item["id"] == id }
487
+ still_blocked = plan["decisions"].any? { |item| item["status"] == "pending" && item.fetch("blocked_tasks", {}).key?(id) }
488
+ task["status"] = prior if task && task["status"] == "waiting_decision" && !still_blocked
489
+ end
490
+ else
491
+ raise ArgumentError, "unknown dynamic plan action"
492
+ end
493
+ end
494
+ end
495
+ end
496
+
497
+ if __FILE__ == $PROGRAM_NAME
498
+ begin
499
+ puts JSON.generate(DynamicPlan.dispatch(Dir.pwd, JSON.parse(STDIN.read)))
500
+ rescue StandardError => error
501
+ puts JSON.generate({ "ok" => false, "reason" => error.message })
502
+ exit 1
503
+ end
504
+ end
@@ -7,6 +7,7 @@ require "yaml"
7
7
  require_relative "validate_state"
8
8
  require_relative "checkpoint_meta"
9
9
  require_relative "gate_policy"
10
+ require_relative "dynamic_plan"
10
11
 
11
12
  options = { json: false, state: nil }
12
13
  OptionParser.new do |opts|
@@ -16,6 +17,11 @@ OptionParser.new do |opts|
16
17
  end.parse!
17
18
 
18
19
  root = Dir.pwd
20
+ dynamic_state = DynamicPlan.read_state(root)
21
+ if dynamic_state && dynamic_state["current_flow"] == "dynamic"
22
+ puts JSON.pretty_generate(DynamicPlan.snapshot(root, dynamic_state))
23
+ exit 0
24
+ end
19
25
  state_path = options[:state] ? File.expand_path(options[:state], root) : File.join(root, ".devflow/planning", "STATE.md")
20
26
 
21
27
  def fail_result(reason, details = {})
@@ -69,6 +69,10 @@ project_lock = DevflowControlIO.acquire_project_lock(root) if options[:write]
69
69
  begin
70
70
  state_path, = DevflowControlIO.resolve_control_path(root, options[:state], :state)
71
71
  state = load_state(state_path)
72
+ if state["current_flow"] == "dynamic"
73
+ puts JSON.generate({ "ok" => true, "written" => false, "mode" => "dynamic", "reason" => "task evidence is maintained in STATE; no per-skill artifact registry required" })
74
+ exit 0
75
+ end
72
76
  ownership = PhaseOwnership.validate_state(root: root, state: state)
73
77
  unless ownership["ok"]
74
78
  puts JSON.pretty_generate(ownership.merge("written" => false))
@@ -60,6 +60,15 @@ module WorkflowStateValidator
60
60
  issues = []
61
61
 
62
62
  state = state_override || load_state(state_path, issues)
63
+ if state && state["current_flow"] == "dynamic"
64
+ require_relative "dynamic_plan"
65
+ begin
66
+ DynamicPlan.validate!(root, state.fetch("plan"))
67
+ rescue ArgumentError, KeyError => error
68
+ issues << issue("error", "invalid_dynamic_plan", error.message)
69
+ end
70
+ return result(issues.empty?, state_path, nil, nil, issues)
71
+ end
63
72
  return result(false, state_path, nil, nil, issues) unless state
64
73
 
65
74
  REQUIRED_STATE_FIELDS.each do |field|
@@ -3,6 +3,9 @@ name: workflow-pipeline
3
3
  description: Use when running or resuming the full .devflow/planning delivery pipeline from requirements through implementation, frozen acceptance tests, and Devflow release closeout.
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  ## 默认软门禁策略
7
10
 
8
11
  工作流硬门禁已移除。下文及各步骤中的产物、索引、合同、审查、质量、恢复和 CP 审批条件均为诊断建议:保留失败证据和缺项,允许继续执行。用户说“下一步”即可推进,无需额外批准弹窗;不得把继续执行记录成人工批准,也不得把失败、未执行的测试改写为通过。真实读写/执行失败、无法解析的状态和并发版本冲突须如实报告。硬门禁及其恢复开关已移除。
@@ -3,6 +3,9 @@ name: workflow-bug-fix
3
3
  description: "Use when a bug must be traced through workflow artifacts, root-caused, fixed, verified, and captured as a lesson when needed."
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
10
 
8
11
  ## Legacy Command Metadata
@@ -3,6 +3,9 @@ name: workflow-execute-phase
3
3
  description: "Use when a planned .devflow/planning phase needs implementation, dependency-aware task execution, progress tracking, and verification evidence."
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
10
 
8
11
  ## Legacy Command Metadata
@@ -13,6 +13,8 @@ autoloadSkills:
13
13
  - code-review
14
14
  ---
15
15
 
16
+ For a Core dynamic-plan review (task kind `review`, mode `dynamic`), read the requirement main document, production source, tests and Core verification evidence directly. Use only applicable review surfaces. Return strict JSON `{ "summary": "evidence inspected and conclusion", "findings": [] }`; findings must include requirement ID, file/line evidence, triggering scenario, impact and recheck. Missing technical evidence is a finding to collect or repair, not a request for the user to manufacture metadata. An empty findings array requires evidence supporting every linked acceptance requirement. The legacy frozen-test-case and REVIEW.md contract below applies only to legacy fixed workflows. Remain read-only.
17
+
16
18
  Review the actual diff and its affected call paths. Prefer concrete defects over style commentary. Treat the parent agent's task summary as scope guidance, never as acceptance authority: verify each expected behavior against confirmed Core requirements/contracts or an explicit human decision, then independently read the relevant source and test evidence at fixed revisions. For each finding provide severity, authoritative expectation reference, source file and location, triggering scenario, impact, evidence status, and a minimal fix direction. Verify whether tests cover the failure. If an essential business fact is absent or conflicting, stop and return the exact question requiring human confirmation; do not infer a clean result.
17
19
 
18
20
  Do not edit code, fix findings, or mutate workflow state. If no actionable finding remains, say so and list the validation evidence inspected.
@@ -3,6 +3,9 @@ name: code-review
3
3
  description: "Use when code needs review for project conventions, spec compliance, behavioral risks, missing tests, change-list completion criteria verification, or post-refactor validation."
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
10
 
8
11
  # 代码规范审查技能
@@ -3,6 +3,9 @@ name: workflow-run-acceptance-tests
3
3
  description: "Execute the frozen TC acceptance suite after code review. Use the requirement baseline and frozen TC nodes for test intent; use current code only to bind execution adapters."
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
10
 
8
11
  # 冻结验收测试执行
@@ -3,6 +3,9 @@ name: workflow-requirement-triage
3
3
  description: "Use as the lightweight first step for a new request, bug report, PRD, or feature idea. Classify the request, check shallow duplicate and prior out-of-scope/rejected decisions, then route a viable request into workflow-wayfinder; do not perform deep requirement validation or route directly to baseline."
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
6
9
  门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
10
 
8
11
  # Workflow: Requirement Triage(需求入口分诊)
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.106";
2
+ export const PRODUCT_VERSION = "0.1.107";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -0,0 +1,31 @@
1
+ /** Presentation only. The revisioned Core plan owns every execution fact. */
2
+ export function renderDynamicWorkflow(snapshot: Record<string, unknown>, showHistory = false): string {
3
+ const tasks = (snapshot.workflow_steps ?? []) as Array<Record<string, unknown>>;
4
+ const marks: Record<string, string> = { completed: "✓", cancelled: "—", pending: "○", running: "●", checking: "●", failed: "✗" };
5
+ const labels: Record<string, string> = { cancelled: "已取消", waiting_decision: "待确认", waiting_external: "等待外部条件", failed: "未通过", checking: "验证中" };
6
+ const visible = !showHistory && tasks.length > 8 ? tasks.filter(task => !["completed", "cancelled"].includes(String(task.status))) : tasks;
7
+ const lines = visible.map(task => `${marks[String(task.status)] ?? "○"} ${task.label}${labels[String(task.status)] ? `(${labels[String(task.status)]})` : ""}${task.verification === "stale" ? "(需要重新验证)" : ""}${task.reason ? `:${task.reason}` : ""}`);
8
+ if (visible.length < tasks.length) lines.push(`已折叠 ${tasks.length - visible.length} 项历史记录,可在 /devflow control 查看全部步骤。`);
9
+ const decisions = (snapshot.decisions ?? []) as Array<Record<string, unknown>>;
10
+ for (const decision of decisions.filter(item => item.status === "pending")) lines.push(`待确认:${decision.question}`);
11
+ return [String(snapshot.current_feature ?? "需求工作流"), `需求说明:${snapshot.document}`, ...lines,
12
+ snapshot.delivery === "ready" ? "验收证据齐备,可以交付。" : snapshot.delivery === "cancelled" ? "需求已取消,历史结果已保留。" : snapshot.reason ? `需要调整计划:${snapshot.reason}` : "",
13
+ ].filter(Boolean).join("\n");
14
+ }
15
+
16
+ export function dynamicWorkflowDirective(snapshot: Record<string, unknown>): string {
17
+ return [
18
+ "<devflow-dynamic-workflow>",
19
+ "读取 skill://workflow-dynamic。当前是动态计划模式,按需求选择工作,不套用固定阶段和逐步审批。",
20
+ "Core STATE 是计划与结果的唯一写入入口。通过 devflow_plan inspect/revise/claim/report/verify 管理任务。",
21
+ "先读需求主文档和下列真实状态。没有计划时,核实项目后在主文档验收要求下维护稳定的 ### R-1: 描述 条目,提交中文任务计划。",
22
+ "已有计划时,执行 Core 选中的任务,读取它的精确 skill。缺项先补证据,技术失败自动修复。完成后继续可执行任务,不逐步询问用户。",
23
+ "只有真实业务取舍或未获授权的操作才调用 devflow_plan decision,给出 2–3 个选项,首项为推荐并说明影响。",
24
+ "已有 running/checking 任务先检查实际产物与执行状态;不得盲目重复外部操作。确认旧执行已结束才 recover。",
25
+ "不生成无关前端任务、空白方案或每技能一份报告。修改需求主文档;源码、测试、API 契约等必要产物照常维护。",
26
+ "验证必须用 verify 运行声明的命令;review 必须派发 devflow-code-reviewer 独立只读 task,返回 JSON {summary:string,findings:array},再用 review_result 提交捕获的原始结果。",
27
+ "Agent 回合结束不等于任务完成;缺失证据、失败、取消与通过必须保持区别。",
28
+ JSON.stringify(snapshot),
29
+ "</devflow-dynamic-workflow>",
30
+ ].join("\n");
31
+ }
@@ -4,14 +4,15 @@ This repository uses an OMP-native, project-scoped software delivery runtime.
4
4
 
5
5
  ## Gate policy
6
6
 
7
- Workflow hard gates have been removed: workflow prerequisites, artifact/index checks, contracts, reviews, acceptance results, recovery checks and CP confirmation are advisory. Preserve failed/blocked results and missing evidence, report risks, and continue executable work. Continuation is never test success or human approval. There is no strict workflow or mandatory CP approval mode. Actual operation errors, unreadable control state and revision conflicts must still be resolved for the affected operation.
7
+ New requirements use dynamic plans: continue dependency-ready work and automatically repair technical findings. Delivery requires current verification and applicable independent review evidence. Preserve failed, stale, waiting and cancelled results; none means passed. Business choices use concise runtime option cards. Actual operation errors, unreadable control state and revision conflicts reject the affected operation. Existing fixed workflows retain their advisory checkpoint policy; do not apply their fixed stage/document requirements to dynamic tasks.
8
8
 
9
9
  ## Control plane
10
10
 
11
11
  - Start only through `/devflow start <feature>|<phase>` after checking `/devflow doctor` and reporting its diagnostics; resume with `/devflow resume`.
12
12
  - Inspect workflow state with `devflow_state`; do not infer state from chat history.
13
13
  - Initialize delivery with `devflow_bootstrap`.
14
- - Mutate checkpoints and steps only with `devflow_transition`.
14
+ - When the inspector reports `mode: dynamic`, load `workflow-dynamic` and use `devflow_plan` for task proposals, execution results and verification. The requirement document owns intent; Core owns progress and observed results. Do not generate irrelevant UI tasks or per-skill Markdown/meta pairs.
15
+ - For existing fixed workflows, mutate checkpoints and steps only with `devflow_transition`.
15
16
  - To abandon an active feature, use the runtime-confirmed `devflow_transition`
16
17
  `cancel-feature` operation or `/devflow abandon`; never edit control files.
17
18
  - Never directly edit `.devflow/planning/STATE.md`, `.devflow/planning/.next-intent`, `.devflow/planning/workflows/`, or `.devflow/planning/artifacts/index.yaml`.
@@ -39,4 +40,4 @@ An index accelerates discovery; it never overrides source code, executable tests
39
40
  - A model assertion is not validation evidence.
40
41
  - Record exact commands and results for tests, builds, migrations, and acceptance checks.
41
42
  - When context reconciliation is requested, follow `skill://workflow-context-sync`, validate the required level, then acknowledge through `devflow_context_sync`.
42
- - Soft checkpoints continue with `continued_with_warning`, including in headless mode. When explicit CP confirmation is enabled, wait for it and never synthesize human approval.
43
+ - Legacy soft checkpoints continue with `continued_with_warning`. Dynamic plans continue from actual task results and never treat an Agent turn ending as successful completion. Never synthesize human approval.