@guanlin4924/devflow-pi 0.1.12 → 0.1.14

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
@@ -29,9 +29,6 @@ devflow-pi
29
29
  /devflow explain-state
30
30
  /devflow recover
31
31
  /devflow start <feature>|<phase>
32
- /devflow continue
33
- /devflow advance
34
- /devflow checkpoint
35
32
  /devflow resume
36
33
  /devflow-usage
37
34
  /usage-stats
@@ -118,7 +115,7 @@ bash .skills/install.sh --apply --force-config
118
115
 
119
116
  首次使用时,在 OMP 的模型角色界面中分别为 `smol`、`task`、`slow` 选择具体模型;`modelRoleStorage: project` 会把选择保存在项目作用域。建议分别使用快速探索模型、代码实现模型和独立推理/审查模型。配置完成后以 `/devflow doctor` 的 `models:` 行为准。
120
117
 
121
- `/devflow start` 只创建 Feature 状态,不会自动启动整条流程。`/devflow continue` 只运行当前一个工作流步骤并停止;产物审查后由用户显式执行 `/devflow advance`,checkpoint 也必须由用户通过 `/devflow checkpoint` 确认。`/devflow explain-state` 发现控制面和制品 registry 漂移时会给出只读恢复计划;用户审查后可用 `/devflow recover` 确认仅修复可由文件系统证明的 registry 字段,它绝不推进步骤或自动通过 checkpoint。恢复已有流程使用 `/devflow resume`,它同样只恢复状态而不自动执行。普通中文输入“初始化项目”“查看状态”“需求:……”会进入相应的受控只读分诊,不会偷偷启动实现或状态迁移。
118
+ `/devflow start` 会先进入安全的需求梳理:可澄清并只读检查,不写入项目。用户随后直接补充需求,原文会被带入入口分诊并启动当前步骤。每个步骤结束后,系统展示产物和证据;用户明确回复“批准执行”才会推进并运行下一步,回复修改意见或“拒绝执行”则保持原步骤。普通需求描述绝不会自动确认 checkpoint。`/devflow explain-state` 发现控制面和制品 registry 漂移时会给出只读恢复计划;用户审查后可用 `/devflow recover` 确认仅修复可由文件系统证明的 registry 字段,它绝不推进步骤或自动通过 checkpoint。恢复已有流程使用 `/devflow resume`,它同样只恢复状态而不自动执行。
122
119
 
123
120
  ## Marketplace 能力包
124
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { mkdir, rename, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
@@ -41,6 +41,12 @@ const READ_ONLY_TOOLS = new Set([
41
41
  "usage_stats",
42
42
  ]);
43
43
 
44
+ const REQUIREMENT_DRAFT_TOOLS = new Set([...READ_ONLY_TOOLS, "ask"]);
45
+ // A Todo update changes only the client-visible task list, never the project
46
+ // or Devflow state. Permit it during planning so a harmless checklist cannot
47
+ // produce a misleading "blocked" warning during initialization.
48
+ const PLANNING_INTERNAL_TOOLS = new Set(["todo"]);
49
+
44
50
  const PLANNING_AGENTS = new Set(["project-scout", "requirements-auditor", "contract-auditor"]);
45
51
 
46
52
  const STATE_MUTATION_TOOLS = new Set([
@@ -91,11 +97,47 @@ export function statusText(state: SessionControlState): string {
91
97
 
92
98
  export function isPlanningToolAllowed(toolName: string, taskAgents: string[] = []): boolean {
93
99
  const normalized = toolName.toLowerCase();
100
+ if (PLANNING_INTERNAL_TOOLS.has(normalized)) return true;
94
101
  if (READ_ONLY_TOOLS.has(normalized)) return true;
95
102
  if (normalized !== "task") return false;
96
103
  return taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
97
104
  }
98
105
 
106
+ /** Requirement drafting may inspect with a shell, but never mutate a project. */
107
+ export function isReadOnlyShellCommand(command: string): boolean {
108
+ return command.trim() !== "" && !shellMutationPaths(command, ".").mutating;
109
+ }
110
+
111
+ export function isRequirementDraftToolAllowed(toolName: string, input: unknown, taskAgents: string[] = []): boolean {
112
+ const normalized = toolName.toLowerCase();
113
+ if (PLANNING_INTERNAL_TOOLS.has(normalized)) return true;
114
+ if (["bash", "shell", "exec_command"].includes(normalized)) {
115
+ const record = input && typeof input === "object" ? input as Record<string, unknown> : {};
116
+ return isReadOnlyShellCommand(String(record.command ?? record.cmd ?? ""));
117
+ }
118
+ if (REQUIREMENT_DRAFT_TOOLS.has(normalized)) return true;
119
+ return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
120
+ }
121
+
122
+ /** `xd://devflow_contract` is an in-memory OMP control resource, not a
123
+ * project file. The planner must be able to read and activate it while the
124
+ * filesystem remains read-only; otherwise initialization deadlocks before it
125
+ * can create a bounded execution contract. */
126
+ export function isInternalDevflowContractResource(toolName: string, input: unknown): boolean {
127
+ if (!["read", "write"].includes(toolName.toLowerCase()) || !input || typeof input !== "object") return false;
128
+ const record = input as Record<string, unknown>;
129
+ return [record.path, record.uri, record.resource].some(value => value === "xd://devflow_contract");
130
+ }
131
+
132
+ export type NaturalControlAction = "approve" | "reject";
133
+
134
+ export function classifyNaturalControlAction(input: string): NaturalControlAction | null {
135
+ const text = input.trim();
136
+ if (/^(?:批准|确认|同意|通过)(?:\s*(?:(?:full|完整|全量)?\s*初始化|执行|继续|推进|当前(?:方案|步骤|计划)))?[。!!]?$/i.test(text)) return "approve";
137
+ if (/^(?:拒绝|不同意|暂不|需要修改|退回)(?:执行|继续|推进|当前(?:方案|步骤|计划))?[。!!]?$/.test(text)) return "reject";
138
+ return null;
139
+ }
140
+
99
141
  export function isStateMutationTool(toolName: string): boolean {
100
142
  return STATE_MUTATION_TOOLS.has(toolName.toLowerCase());
101
143
  }
@@ -230,6 +272,7 @@ const REQUIRED_AGENT_NAMES = new Set(Object.keys(REQUIRED_AGENTS));
230
272
  const REQUIRED_SCRIPTS = [
231
273
  "workflow-orchestrator/scripts/init_workflow.rb",
232
274
  "workflow-orchestrator/scripts/init_state.rb",
275
+ "workflow-orchestrator/scripts/phase_ownership.rb",
233
276
  "workflow-orchestrator/scripts/inspect_state.rb",
234
277
  "workflow-orchestrator/scripts/advance_state.rb",
235
278
  "workflow-orchestrator/scripts/sync_artifacts.rb",
@@ -237,6 +280,17 @@ const REQUIRED_SCRIPTS = [
237
280
  "workflow-context-sync/scripts/context_sync_cursor.py",
238
281
  ];
239
282
 
283
+ function defaultPhaseId(feature: string): string {
284
+ // STATE.md is project-global, so a generic name such as "delivery" is not
285
+ // a safe default. Keep the generated id filesystem-safe and deterministic
286
+ // without requiring a user to learn a phase-naming command.
287
+ return `feature-${createHash("sha256").update(feature, "utf8").digest("hex").slice(0, 12)}`;
288
+ }
289
+
290
+ function validPhaseId(phase: string): boolean {
291
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(phase);
292
+ }
293
+
240
294
  function resolvePluginRoot(): string {
241
295
  const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
242
296
  return path.resolve(process.env.DEVFLOW_PLUGIN_ROOT || path.resolve(moduleDirectory, "../.."));
@@ -464,6 +518,13 @@ function parseJsonResult(result: ProcessResult): Record<string, unknown> | null
464
518
  }
465
519
  }
466
520
 
521
+ /** Ruby control-plane scripts report expected validation failures as JSON with
522
+ * `ok: false` and intentionally keep a zero process exit code. Never mistake
523
+ * that structured failure for a successful state mutation. */
524
+ function controlPlaneSucceeded(result: ProcessResult): boolean {
525
+ return result.ok && parseJsonResult(result)?.ok === true;
526
+ }
527
+
467
528
  function compactStep(value: unknown): Record<string, unknown> | null {
468
529
  if (!value || typeof value !== "object") return null;
469
530
  const step = value as Record<string, unknown>;
@@ -488,7 +549,7 @@ function compactStep(value: unknown): Record<string, unknown> | null {
488
549
  };
489
550
  }
490
551
 
491
- function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "resume" | "continue"): string {
552
+ function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
492
553
  const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
493
554
  const envelope = {
494
555
  source,
@@ -502,19 +563,31 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
502
563
  failure_branch: snapshot.failure_branch,
503
564
  reason: snapshot.reason,
504
565
  recovery: snapshot.recovery,
566
+ requirement_draft: requirementDraft,
505
567
  };
506
568
  return [
507
569
  "<devflow-runtime-directive>",
508
570
  "Devflow 已通过启动预检。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
509
571
  "先调用 devflow_state(action=\"inspect\") 重新确认 revision,再由 workflow-orchestrator 路由当前步骤。",
510
572
  "只按 inspector 返回的当前 step 加载所需 Skill;不得调用 devflow_transition、不得推进下一步骤、不得替用户通过人工 checkpoint。",
511
- "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止,等待用户执行 /devflow advance。",
573
+ "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
512
574
  "当前步骤信封:",
513
575
  JSON.stringify(envelope, null, 2),
514
576
  "</devflow-runtime-directive>",
515
577
  ].join("\n");
516
578
  }
517
579
 
580
+ function projectInitializationDirective(summary: string): string {
581
+ return [
582
+ "<devflow-project-initialization-directive>",
583
+ "用户已批准完整项目初始化。现在处于执行模式;不要再创建 Todo、不要读取或写入 xd://devflow_contract。",
584
+ "执行 skill://workflow-project-context,按已确认的项目边界建立 .planning/PROJECT.md、.planning/navigation/INDEX.md、.context/spec/index.md,并按用户选择建立 full 代码索引。",
585
+ "只写项目初始化所需的 .planning/ 与 .context/ 控制产物;不得改动业务源码、依赖或配置。完成后运行项目初始化校验,报告生成文件和验证结果并停止,等待用户审查。",
586
+ summary,
587
+ "</devflow-project-initialization-directive>",
588
+ ].join("\n");
589
+ }
590
+
518
591
  function explainWorkflowSnapshot(snapshot: Record<string, unknown> | null): string {
519
592
  if (!snapshot) return "Devflow 当前没有可解释的工作流状态。可使用 /devflow start <feature> 创建新 Feature。";
520
593
  const step = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
@@ -853,8 +926,12 @@ export default function devflowCore(pi: ExtensionAPI) {
853
926
  return { result, snapshot: parseJsonResult(result) };
854
927
  }
855
928
 
856
- async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "resume" | "continue") {
857
- await pi.sendUserMessage(workflowDirective(snapshot, source));
929
+ let activeWorkflowTurn = false;
930
+ let pendingProjectInitialization: string | undefined;
931
+
932
+ async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string) {
933
+ activeWorkflowTurn = true;
934
+ await pi.sendUserMessage(workflowDirective(snapshot, source, requirementDraft));
858
935
  }
859
936
 
860
937
  pi.setLabel("Devflow Project Runtime");
@@ -879,8 +956,84 @@ export default function devflowCore(pi: ExtensionAPI) {
879
956
  systemPrompt: [...event.systemPrompt, DEVFLOW_CHINESE_LANGUAGE_POLICY],
880
957
  }));
881
958
 
959
+ pi.on("agent_end", async (event, ctx) => {
960
+ if (!activeWorkflowTurn || event.willContinue) return;
961
+ activeWorkflowTurn = false;
962
+ if (controlState.phase === "executing") {
963
+ setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
964
+ ctx.ui.notify("当前步骤已结束。请审查输出:明确回复“批准执行”推进,或回复修改意见/“拒绝执行”保持当前步骤。", "info");
965
+ }
966
+ });
967
+
882
968
  pi.on("input", async (event, ctx) => {
883
969
  if (event.source !== "interactive") return;
970
+ const controlAction = classifyNaturalControlAction(event.text);
971
+ if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
972
+ const requirementDraft = event.text.trim();
973
+ if (!requirementDraft) return { handled: true };
974
+ const inspected = await inspectWorkflow(ctx.cwd);
975
+ if (!inspected.result.ok || !inspected.snapshot) {
976
+ ctx.ui.notify(`需求分诊启动失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
977
+ return { handled: true };
978
+ }
979
+ setControlState(ctx, executingState("执行需求入口分诊"));
980
+ await triggerWorkflow(inspected.snapshot, "start", requirementDraft);
981
+ return { handled: true };
982
+ }
983
+ if (controlState.intent === "project-init" && controlState.phase === "planning") {
984
+ if (controlAction === "approve") {
985
+ if (!pendingProjectInitialization) {
986
+ ctx.ui.notify("项目初始化缺少已确认的边界信息,请重新发起初始化。", "error");
987
+ return { handled: true };
988
+ }
989
+ setControlState(ctx, executingState("执行已批准的项目初始化"));
990
+ activeWorkflowTurn = true;
991
+ return { text: projectInitializationDirective(pendingProjectInitialization), handled: false };
992
+ }
993
+ if (controlAction === "reject") {
994
+ ctx.ui.notify("已保留初始化规划,未创建任何项目控制文件。请直接说明需要调整的初始化范围。", "info");
995
+ return { handled: true };
996
+ }
997
+ }
998
+ if (controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
999
+ if (controlAction === "reject") {
1000
+ ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
1001
+ return { handled: true };
1002
+ }
1003
+ if (controlAction === "approve") {
1004
+ const inspected = await inspectWorkflow(ctx.cwd);
1005
+ if (!inspected.result.ok || !inspected.snapshot) {
1006
+ ctx.ui.notify(`审批处理失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1007
+ return { handled: true };
1008
+ }
1009
+ const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
1010
+ const checkpoint = inspected.snapshot.status === "waiting_checkpoint";
1011
+ const step = compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step);
1012
+ const stepId = typeof step?.id === "string" ? step.id : undefined;
1013
+ if (revision === undefined || (!checkpoint && !stepId)) {
1014
+ ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
1015
+ return { handled: true };
1016
+ }
1017
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1018
+ "--write", "--json", "--expected-revision", String(revision),
1019
+ ...(checkpoint ? ["--confirm-checkpoint"] : ["--complete-step", stepId!]),
1020
+ ]);
1021
+ if (!result.ok) {
1022
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
1023
+ ctx.ui.notify(`未推进:${result.stderr || result.stdout}`, "error");
1024
+ return { handled: true };
1025
+ }
1026
+ const after = await inspectWorkflow(ctx.cwd);
1027
+ if (!after.result.ok || !after.snapshot || after.snapshot.action === "workflow_complete") {
1028
+ setControlState(ctx, initialControlState());
1029
+ ctx.ui.notify(after.snapshot?.action === "workflow_complete" ? "工作流已完成。" : "已推进,但读取下一步骤失败。", "info");
1030
+ return { handled: true };
1031
+ }
1032
+ setControlState(ctx, executingState("执行已批准的下一工作流步骤"));
1033
+ await triggerWorkflow(after.snapshot, "approval");
1034
+ return { handled: true };
1035
+ }
1036
+ }
884
1037
  const intent = classifyNaturalLanguageIntent(event.text);
885
1038
  if (intent === "explain-state") {
886
1039
  const inspected = await inspectWorkflow(ctx.cwd);
@@ -894,6 +1047,7 @@ export default function devflowCore(pi: ExtensionAPI) {
894
1047
  if (intent === "project-init") {
895
1048
  const summary = await collectProjectInitialization(ctx);
896
1049
  if (!summary) return { handled: true };
1050
+ pendingProjectInitialization = summary;
897
1051
  const state = planningState("project-init");
898
1052
  setControlState(ctx, state);
899
1053
  return {
@@ -913,7 +1067,7 @@ export default function devflowCore(pi: ExtensionAPI) {
913
1067
  return {
914
1068
  text: [
915
1069
  "<devflow-planner-directive>",
916
- "当前是需求梳理(只读)阶段。只允许澄清、收集证据、派发只读审查代理和输出计划草案。",
1070
+ "当前是需求梳理(只读)阶段。可澄清、收集证据、派发只读审查代理,且可使用不修改文件的 shell 检查。",
917
1071
  "不得开始工作流、不得写入产物、不得调用实现代理或状态迁移。",
918
1072
  "把用户目标、约束、未知项、反例和下一步最少问题分开列出;缺少证据时不得猜测。",
919
1073
  event.text,
@@ -944,7 +1098,10 @@ export default function devflowCore(pi: ExtensionAPI) {
944
1098
  if (shadows.length > 0) {
945
1099
  return { block: true, reason: `Devflow task agent shadow detected: ${shadows.join("; ")}` };
946
1100
  }
947
- if (controlState.phase === "planning" && !isPlanningToolAllowed(toolName, requested.names)) {
1101
+ const allowedInDraft = controlState.intent === "requirement-draft"
1102
+ ? isRequirementDraftToolAllowed(toolName, event.input, requested.names)
1103
+ : isPlanningToolAllowed(toolName, requested.names);
1104
+ if (controlState.phase === "planning" && !allowedInDraft) {
948
1105
  return { block: true, reason: planningToolBlockReason(toolName) };
949
1106
  }
950
1107
  if (
@@ -957,8 +1114,11 @@ export default function devflowCore(pi: ExtensionAPI) {
957
1114
  } else if (
958
1115
  controlState.phase === "planning" &&
959
1116
  toolName !== "devflow_contract" &&
1117
+ !isInternalDevflowContractResource(toolName, event.input) &&
960
1118
  !(toolName === "devflow_evidence" && String((event.input as Record<string, unknown> | undefined)?.action) !== "create") &&
961
- !isPlanningToolAllowed(toolName)
1119
+ !(controlState.intent === "requirement-draft"
1120
+ ? isRequirementDraftToolAllowed(toolName, event.input)
1121
+ : isPlanningToolAllowed(toolName))
962
1122
  ) {
963
1123
  return { block: true, reason: planningToolBlockReason(toolName) };
964
1124
  }
@@ -966,7 +1126,7 @@ export default function devflowCore(pi: ExtensionAPI) {
966
1126
  return {
967
1127
  block: true,
968
1128
  reason:
969
- "Devflow state transitions are user-command-only. Use /devflow start, /devflow advance, or /devflow checkpoint; an LLM may not advance delivery state directly.",
1129
+ "Devflow 状态迁移只能由运行时在用户明确批准已展示的步骤或 checkpoint 后执行;LLM 不得自行推进。",
970
1130
  };
971
1131
  }
972
1132
  if (toolName.startsWith("devflow_")) return;
@@ -1246,35 +1406,54 @@ export default function devflowCore(pi: ExtensionAPI) {
1246
1406
  description: "Initialize workflow definitions, STATE.md, and the artifact registry for a project feature.",
1247
1407
  parameters: z.object({
1248
1408
  feature: z.string().min(1),
1249
- phase: z.string().min(1).default("delivery"),
1409
+ phase: z.string().min(1).optional(),
1250
1410
  flow: z.string().min(1).default("feature-development"),
1251
1411
  force: z.boolean().default(false),
1252
1412
  }),
1253
1413
  approval: "write",
1254
1414
  async execute(_id, params, _signal, _onUpdate, ctx) {
1415
+ const phase = params.phase?.trim() || defaultPhaseId(params.feature);
1416
+ if (!validPhaseId(phase)) {
1417
+ return {
1418
+ content: [{ type: "text" as const, text: "Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。" }],
1419
+ isError: true,
1420
+ };
1421
+ }
1255
1422
  const force = params.force ? ["--force"] : [];
1423
+ const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
1424
+ "claim",
1425
+ "--feature",
1426
+ params.feature,
1427
+ "--phase",
1428
+ phase,
1429
+ "--flow",
1430
+ params.flow,
1431
+ "--json",
1432
+ ]);
1433
+ if (!controlPlaneSucceeded(ownership)) return renderResult(ownership, { stage: "phase-ownership" });
1256
1434
  const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
1257
1435
  "--write",
1258
1436
  "--json",
1259
1437
  ...force,
1260
1438
  ]);
1261
- if (!workflow.ok) return renderResult(workflow, { stage: "workflow" });
1439
+ if (!controlPlaneSucceeded(workflow)) return renderResult(workflow, { stage: "workflow" });
1262
1440
  const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
1263
1441
  "--feature",
1264
1442
  params.feature,
1265
1443
  "--phase",
1266
- params.phase,
1444
+ phase,
1267
1445
  "--flow",
1268
1446
  params.flow,
1269
1447
  "--write",
1270
1448
  "--json",
1271
1449
  ...force,
1272
1450
  ]);
1273
- if (!state.ok) return renderResult(state, { stage: "state" });
1451
+ if (!controlPlaneSucceeded(state)) return renderResult(state, { stage: "state" });
1274
1452
  const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
1275
1453
  "--write",
1276
1454
  "--json",
1277
1455
  ]);
1456
+ if (!controlPlaneSucceeded(artifacts)) return renderResult(artifacts, { stage: "artifacts" });
1278
1457
  return renderResult(artifacts, {
1279
1458
  stage: "complete",
1280
1459
  workflow: workflow.stdout,
@@ -1359,7 +1538,8 @@ export default function devflowCore(pi: ExtensionAPI) {
1359
1538
  approval: "write",
1360
1539
  async execute(_id, params, _signal, _onUpdate, ctx) {
1361
1540
  const args = ["--write", "--confirm", "--expected-revision", String(params.expectedRevision), "--json"];
1362
- if (params.state) args.push("--state", params.state);
1541
+ const state = safeControlPath(ctx.cwd, params.state, "state");
1542
+ if (state) args.push("--state", state);
1363
1543
  return renderResult(
1364
1544
  await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", args),
1365
1545
  { action: "repair-control-plane" },
@@ -1444,9 +1624,9 @@ export default function devflowCore(pi: ExtensionAPI) {
1444
1624
  const step = compactStep(inspected.snapshot?.next_step) ?? compactStep(inspected.snapshot?.current_step);
1445
1625
  const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
1446
1626
  if (inspected.result.ok && status === "in_progress") {
1447
- ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请执行 /devflow continue。`, "info");
1627
+ ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
1448
1628
  } else if (inspected.result.ok && status === "waiting_checkpoint") {
1449
- ctx.ui.notify(`控制面状态一致,无需恢复。当前正在等待 checkpoint;请审查产物后执行 /devflow checkpoint。`, "info");
1629
+ ctx.ui.notify("控制面状态一致,无需恢复。当前正在等待 checkpoint;请审查产物后明确回复“批准执行”或提出修改意见。", "info");
1450
1630
  } else {
1451
1631
  ctx.ui.notify("当前状态没有可安全自动执行的恢复计划;请运行 /devflow explain-state,并按其中的阻塞证据处理。", "error");
1452
1632
  }
@@ -1471,12 +1651,13 @@ export default function devflowCore(pi: ExtensionAPI) {
1471
1651
  ctx.ui.notify(`控制面恢复失败:${result.stderr || result.stdout}`, "error");
1472
1652
  return;
1473
1653
  }
1474
- ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后使用 /devflow advance。", "info");
1654
+ ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
1475
1655
  return;
1476
1656
  }
1477
1657
  if (trimmed === "project-init") {
1478
1658
  const summary = await collectProjectInitialization(ctx);
1479
1659
  if (!summary) return;
1660
+ pendingProjectInitialization = summary;
1480
1661
  setControlState(ctx, planningState("project-init"));
1481
1662
  await pi.sendUserMessage(
1482
1663
  [
@@ -1515,138 +1696,22 @@ export default function devflowCore(pi: ExtensionAPI) {
1515
1696
  }
1516
1697
  setControlState(ctx, waitingUserState("等待用户审查步骤产物"));
1517
1698
  ctx.ui.notify(
1518
- `Devflow 已恢复到 ${String((compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step))?.id ?? "当前步骤")};未自动执行。请先查看状态或使用 /devflow continue 执行一个步骤。`,
1699
+ `Devflow 已恢复到 ${String((compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step))?.id ?? "当前步骤")};请审查当前产物后明确回复“批准执行”,或直接提出修改意见。`,
1519
1700
  "info",
1520
1701
  );
1521
1702
  return;
1522
1703
  }
1523
- if (trimmed === "continue") {
1524
- const report = await runDoctor(ctx);
1525
- if (!report.ok) {
1526
- ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
1527
- return;
1528
- }
1529
- const inspected = await inspectWorkflow(ctx.cwd);
1530
- if (!inspected.result.ok || !inspected.snapshot) {
1531
- ctx.ui.notify(`Devflow continue failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1532
- return;
1533
- }
1534
- if (inspected.snapshot.action === "workflow_complete") {
1535
- ctx.ui.notify("Devflow workflow is already complete.", "info");
1536
- return;
1537
- }
1538
- if (inspected.snapshot.status === "waiting_checkpoint") {
1539
- setControlState(ctx, { intent: "workflow-execute", phase: "awaiting-approval", label: "等待用户确认 checkpoint" });
1540
- ctx.ui.notify("当前步骤正在等待 checkpoint;请审查产物后使用 /devflow checkpoint。", "warning");
1541
- return;
1542
- }
1543
- setControlState(ctx, executingState("执行当前工作流步骤(完成后停止)"));
1544
- ctx.ui.notify("仅启动当前步骤;模型被禁止迁移状态或继续下一步骤。", "info");
1545
- await triggerWorkflow(inspected.snapshot, "continue");
1546
- return;
1547
- }
1548
- if (trimmed === "advance") {
1549
- const inspected = await inspectWorkflow(ctx.cwd);
1550
- if (!inspected.result.ok || !inspected.snapshot) {
1551
- ctx.ui.notify(`Devflow advance failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1552
- return;
1553
- }
1554
- if (inspected.snapshot.action === "workflow_complete" && inspected.snapshot.source_flow) {
1555
- const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
1556
- if (revision === undefined) {
1557
- ctx.ui.notify("补救分支缺少 revision,拒绝返回主流程。", "error");
1558
- return;
1559
- }
1560
- const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1561
- "--write",
1562
- "--json",
1563
- "--expected-revision",
1564
- String(revision),
1565
- "--exit-failure-branch",
1566
- ]);
1567
- if (!result.ok) {
1568
- ctx.ui.notify(`补救分支返回失败:${result.stderr || result.stdout}`, "error");
1569
- return;
1570
- }
1571
- ctx.ui.notify("补救分支已返回主流程;仍需审查 reverify 结果,并在 CP-4 明确确认后才能继续。", "info");
1572
- return;
1573
- }
1574
- if (inspected.snapshot.status === "waiting_checkpoint") {
1575
- ctx.ui.notify("当前状态等待 checkpoint;请使用 /devflow checkpoint。", "warning");
1576
- return;
1577
- }
1578
- const step = compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step);
1579
- const stepId = typeof step?.id === "string" ? step.id : undefined;
1580
- const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
1581
- if (!stepId || revision === undefined) {
1582
- ctx.ui.notify("当前状态没有可推进的步骤或 revision;请先执行 /devflow explain-state。", "error");
1583
- return;
1584
- }
1585
- const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1586
- "--write",
1587
- "--json",
1588
- "--expected-revision",
1589
- String(revision),
1590
- "--complete-step",
1591
- stepId,
1592
- ]);
1593
- if (!result.ok) {
1594
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
1595
- ctx.ui.notify(`步骤未推进:${result.stderr || result.stdout}`, "error");
1596
- return;
1597
- }
1598
- const after = await inspectWorkflow(ctx.cwd);
1599
- setControlState(
1600
- ctx,
1601
- after.snapshot?.status === "waiting_checkpoint"
1602
- ? { intent: "workflow-execute", phase: "awaiting-approval", label: "等待用户确认 checkpoint" }
1603
- : waitingUserState("等待用户审查步骤产物"),
1604
- );
1605
- ctx.ui.notify(`已完成步骤 ${stepId} 的真实产物校验并推进状态。${after.snapshot?.status === "waiting_checkpoint" ? "下一步需要 /devflow checkpoint。" : "下一步请审查后使用 /devflow continue。"}`, "info");
1606
- return;
1607
- }
1608
- if (trimmed === "checkpoint") {
1609
- const inspected = await inspectWorkflow(ctx.cwd);
1610
- if (!inspected.result.ok || !inspected.snapshot) {
1611
- ctx.ui.notify(`Devflow checkpoint failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1612
- return;
1613
- }
1614
- if (inspected.snapshot.status !== "waiting_checkpoint") {
1615
- ctx.ui.notify("当前并不等待 checkpoint;请使用 /devflow explain-state 查看原因。", "warning");
1616
- return;
1617
- }
1618
- const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
1619
- if (revision === undefined) {
1620
- ctx.ui.notify("当前状态缺少 revision,拒绝确认 checkpoint。", "error");
1621
- return;
1622
- }
1623
- const confirmed = await ctx.ui.confirm("确认 Devflow checkpoint", "你已审查当前步骤产物并同意推进到下一步骤吗?");
1624
- if (!confirmed) {
1625
- ctx.ui.notify("checkpoint 未确认;状态保持不变。", "info");
1626
- return;
1627
- }
1628
- const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1629
- "--write",
1630
- "--json",
1631
- "--expected-revision",
1632
- String(revision),
1633
- "--confirm-checkpoint",
1634
- ]);
1635
- if (!result.ok) {
1636
- setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "checkpoint 校验失败" });
1637
- ctx.ui.notify(`checkpoint 未推进:${result.stderr || result.stdout}`, "error");
1638
- return;
1639
- }
1640
- setControlState(ctx, waitingUserState("等待用户确认下一步骤"));
1641
- ctx.ui.notify("checkpoint 已由用户确认;下一步骤尚未执行,请使用 /devflow continue。", "info");
1642
- return;
1643
- }
1644
1704
  if (trimmed.startsWith("start ")) {
1645
- const [feature, phase = "delivery"] = trimmed.slice(6).split("|", 2).map(value => value.trim());
1705
+ const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
1646
1706
  if (!feature) {
1647
1707
  ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
1648
1708
  return;
1649
1709
  }
1710
+ const phase = requestedPhase || defaultPhaseId(feature);
1711
+ if (!validPhaseId(phase)) {
1712
+ ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
1713
+ return;
1714
+ }
1650
1715
  const report = await runDoctor(ctx);
1651
1716
  if (!report.ok) {
1652
1717
  ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
@@ -1661,16 +1726,33 @@ export default function devflowCore(pi: ExtensionAPI) {
1661
1726
  existingFeature !== feature
1662
1727
  ) {
1663
1728
  ctx.ui.notify(
1664
- `An unfinished Devflow state belongs to feature ${existingFeature}. Use /devflow resume and complete that workflow before starting ${feature}.`,
1729
+ `当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
1665
1730
  "error",
1666
1731
  );
1667
1732
  return;
1668
1733
  }
1734
+ // This dry-run happens before init_workflow.rb, so a rejected feature
1735
+ // can never overwrite generated workflow files belonging to an active
1736
+ // owner or a legacy phase directory that has no trustworthy owner.
1737
+ const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
1738
+ "claim",
1739
+ "--feature",
1740
+ feature,
1741
+ "--phase",
1742
+ phase,
1743
+ "--flow",
1744
+ "feature-development",
1745
+ "--json",
1746
+ ]);
1747
+ if (!controlPlaneSucceeded(ownership)) {
1748
+ ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
1749
+ return;
1750
+ }
1669
1751
  const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
1670
1752
  "--write",
1671
1753
  "--json",
1672
1754
  ]);
1673
- if (!workflow.ok) {
1755
+ if (!controlPlaneSucceeded(workflow)) {
1674
1756
  ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
1675
1757
  return;
1676
1758
  }
@@ -1685,7 +1767,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1685
1767
  "--json",
1686
1768
  ...(previousWorkflowComplete ? ["--force"] : []),
1687
1769
  ]);
1688
- if (!state.ok) {
1770
+ if (!controlPlaneSucceeded(state)) {
1689
1771
  ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
1690
1772
  return;
1691
1773
  }
@@ -1693,7 +1775,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1693
1775
  "--write",
1694
1776
  "--json",
1695
1777
  ]);
1696
- if (!artifacts.ok) {
1778
+ if (!controlPlaneSucceeded(artifacts)) {
1697
1779
  ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
1698
1780
  return;
1699
1781
  }
@@ -1706,14 +1788,19 @@ export default function devflowCore(pi: ExtensionAPI) {
1706
1788
  return;
1707
1789
  }
1708
1790
  setControlState(ctx, planningState("requirement-draft"));
1709
- ctx.ui.notify(
1710
- `Devflow 已创建 Feature ${feature},但没有自动执行。当前步骤:${String((compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step))?.id ?? "requirement-triage")}。请补充需求后使用 /devflow continue。`,
1711
- "info",
1791
+ await pi.sendUserMessage(
1792
+ [
1793
+ "<devflow-requirement-draft-directive>",
1794
+ `Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
1795
+ "请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
1796
+ "不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
1797
+ "</devflow-requirement-draft-directive>",
1798
+ ].join("\n"),
1712
1799
  );
1713
1800
  return;
1714
1801
  }
1715
1802
  ctx.ui.notify(
1716
- "Usage: /devflow doctor | status | explain-state | recover | project-init | start <feature>|<phase> | resume | continue | advance | checkpoint",
1803
+ "Usage: /devflow doctor | status | explain-state | recover | project-init | start <feature>|<phase> | resume",
1717
1804
  "warning",
1718
1805
  );
1719
1806
  },