@guanlin4924/devflow-pi 0.1.26 → 0.1.27

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.26
14
+ npm install -g @guanlin4924/devflow-pi@0.1.27
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.26",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -2403,9 +2403,11 @@ export default function devflowCore(pi: ExtensionAPI) {
2403
2403
  label: "Devflow Transition",
2404
2404
  description: "Apply one validated delivery-state transition. This is the only supported STATE.md mutation path.",
2405
2405
  parameters: z.object({
2406
- action: z.enum(["complete-step", "confirm-checkpoint", "checkpoint-decision", "enter-failure-branch"]),
2406
+ action: z.enum(["complete-step", "confirm-checkpoint", "checkpoint-decision", "enter-failure-branch", "cancel-feature"]),
2407
2407
  step: z.string().optional(),
2408
2408
  decision: z.string().optional(),
2409
+ reason: z.string().optional(),
2410
+ confirm: z.literal(true).optional(),
2409
2411
  state: z.string().optional(),
2410
2412
  expectedRevision: z.number().int().min(0),
2411
2413
  }),
@@ -2438,6 +2440,19 @@ export default function devflowCore(pi: ExtensionAPI) {
2438
2440
  return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "decision is required" });
2439
2441
  }
2440
2442
  args.push("--checkpoint-decision", params.decision);
2443
+ } else if (params.action === "cancel-feature") {
2444
+ const inspected = await inspectWorkflow(ctx.cwd);
2445
+ const snapshot = inspected.snapshot;
2446
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : "当前需求";
2447
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
2448
+ if (!inspected.result.ok || revision === undefined) return renderResult(inspected.result, { action: params.action });
2449
+ if (!params.reason?.trim()) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "reason is required" });
2450
+ const confirmed = await ctx.ui.confirm(
2451
+ "确认弃用当前需求",
2452
+ `需求:${feature}\n原因:${params.reason.trim()}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
2453
+ );
2454
+ if (!confirmed) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "User declined feature cancellation." });
2455
+ args.push("--cancel", "--cancel-reason", params.reason.trim());
2441
2456
  } else {
2442
2457
  args.push("--enter-failure-branch");
2443
2458
  }
@@ -2601,6 +2616,42 @@ export default function devflowCore(pi: ExtensionAPI) {
2601
2616
  ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
2602
2617
  return;
2603
2618
  }
2619
+ if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
2620
+ const inspected = await inspectWorkflow(ctx.cwd);
2621
+ const snapshot = inspected.snapshot;
2622
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
2623
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
2624
+ const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
2625
+ if (!inspected.result.ok || revision === undefined || !feature) {
2626
+ ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
2627
+ return;
2628
+ }
2629
+ if (!status || ["completed", "cancelled", "failed"].includes(status)) {
2630
+ ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
2631
+ return;
2632
+ }
2633
+ const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
2634
+ const confirmed = await ctx.ui.confirm(
2635
+ "确认弃用当前需求",
2636
+ `需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
2637
+ );
2638
+ if (!confirmed) {
2639
+ ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
2640
+ return;
2641
+ }
2642
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
2643
+ "--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
2644
+ ]);
2645
+ if (!result.ok) {
2646
+ ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
2647
+ return;
2648
+ }
2649
+ activeContract = undefined;
2650
+ contractAgentCalls = 0;
2651
+ setControlState(ctx, initialControlState());
2652
+ ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
2653
+ return;
2654
+ }
2604
2655
  if (trimmed === "project-init") {
2605
2656
  const initialization = await collectProjectInitialization(ctx);
2606
2657
  if (!initialization) return;
@@ -2672,7 +2723,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2672
2723
  }
2673
2724
  const existing = await inspectWorkflow(ctx.cwd);
2674
2725
  const existingFeature = existing.snapshot?.current_feature;
2675
- const previousWorkflowComplete = existing.snapshot?.action === "workflow_complete";
2726
+ const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
2676
2727
  if (
2677
2728
  !previousWorkflowComplete &&
2678
2729
  typeof existingFeature === "string" &&
@@ -2753,7 +2804,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2753
2804
  return;
2754
2805
  }
2755
2806
  ctx.ui.notify(
2756
- "Usage: /devflow doctor | status | explain-state | recover | project-init | start <feature>|<phase> | resume",
2807
+ "Usage: /devflow doctor | status | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
2757
2808
  "warning",
2758
2809
  );
2759
2810
  },
@@ -21,6 +21,8 @@ options = {
21
21
  checkpoint_decision: nil,
22
22
  enter_failure_branch: false,
23
23
  exit_failure_branch: false,
24
+ cancel: false,
25
+ cancel_reason: nil,
24
26
  expected_revision: nil,
25
27
  json: false
26
28
  }
@@ -34,6 +36,8 @@ OptionParser.new do |opts|
34
36
  opts.on("--checkpoint-decision VALUE", "Set checkpoint decision, e.g. approved, rejected, needs_fix") { |value| options[:checkpoint_decision] = value }
35
37
  opts.on("--enter-failure-branch", "Transition into the configured failure branch for the current step") { options[:enter_failure_branch] = true }
36
38
  opts.on("--exit-failure-branch", "Return from a completed failure branch to its configured source flow") { options[:exit_failure_branch] = true }
39
+ opts.on("--cancel", "Cancel the active feature without marking pending steps completed") { options[:cancel] = true }
40
+ opts.on("--cancel-reason REASON", "Record why the active feature was cancelled") { |reason| options[:cancel_reason] = reason }
37
41
  opts.on("--expected-revision REVISION", Integer, "Reject a stale write unless STATE.md has this revision") { |value| options[:expected_revision] = value }
38
42
  opts.on("--json", "Print JSON output") { options[:json] = true }
39
43
  end.parse!
@@ -527,6 +531,31 @@ result =
527
531
  action_description = "Checkpoint decision set to: #{options[:checkpoint_decision]}"
528
532
  end
529
533
 
534
+ if result.nil? && options[:cancel]
535
+ if %w[completed cancelled failed].include?(state["status"])
536
+ result = fail_result("feature_not_active", "status" => state["status"])
537
+ elsif options[:cancel_reason].to_s.strip.empty?
538
+ result = fail_result("cancel_reason_required")
539
+ else
540
+ state["status"] = "cancelled"
541
+ state["cancel_reason"] = options[:cancel_reason].to_s.strip
542
+ state["cancelled_at"] = Date.today.to_s
543
+ # A cancelled feature is terminal. Preserve completed steps and the
544
+ # previous step for history, but clear current/pending work so the
545
+ # inspector can expose a terminal state and ownership can be released.
546
+ state["cancelled_step"] = state["current_step"]
547
+ state["current_step"] = nil
548
+ state["pending_steps"] = []
549
+ state.delete("checkpoint")
550
+ state.delete("checkpoint_label")
551
+ state.delete("checkpoint_decision")
552
+ state.delete("rollover")
553
+ changed = true
554
+ actions << "feature_cancelled"
555
+ action_description = "Feature cancelled: #{state["current_feature"]}. Reason: #{state["cancel_reason"]}."
556
+ end
557
+ end
558
+
530
559
  if result.nil? && options[:complete_step]
531
560
  step_id = options[:complete_step]
532
561
  step = step_by_id[step_id]
@@ -473,6 +473,28 @@ result =
473
473
  action: "workflow_complete",
474
474
  reason: "completed_status_with_no_pending_steps"
475
475
  }
476
+ elsif pending_ids.empty? && %w[cancelled failed].include?(state["status"])
477
+ {
478
+ current_flow: state["current_flow"],
479
+ current_feature: state["current_feature"],
480
+ current_phase: state["current_phase"],
481
+ status: state["status"],
482
+ cancel_reason: state["cancel_reason"],
483
+ cancelled_step: state["cancelled_step"],
484
+ completed_steps: completed_ids,
485
+ pending_steps: pending_ids,
486
+ current_step: state["current_step"] && step_payload(step_by_id[state["current_step"]], state, root, artifact_by_path),
487
+ next_step: nil,
488
+ workflow_config: workflow_path,
489
+ artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
490
+ "path" => artifact_registry_path,
491
+ "error" => artifact_registry_error,
492
+ "recovery" => artifact_registry_error ? "Call devflow_artifact with action=sync." : nil,
493
+ "current_outputs_registered" => 0
494
+ ),
495
+ action: state["status"] == "cancelled" ? "workflow_cancelled" : "workflow_failed",
496
+ reason: "terminal_status_with_no_pending_steps"
497
+ }
476
498
  else
477
499
  fail_result("unknown_current_step", current_step: state["current_step"])
478
500
  end
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.26";
2
+ export const PRODUCT_VERSION = "0.1.27";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -10,6 +10,8 @@ export interface PlatformExtensionOptions {
10
10
  sessionRoot: string;
11
11
  ompSessionRoot: string;
12
12
  projectId: string;
13
+ /** Read the persisted role assignment so optional roles can fail closed. */
14
+ getConfiguredRole?: (role: string) => string | undefined;
13
15
  }
14
16
 
15
17
  type UsageFormat = "table" | "json" | "html";
@@ -55,6 +57,45 @@ export function createPlatformExtension(options: PlatformExtensionOptions) {
55
57
  return function devflowPlatform(pi: ExtensionAPI) {
56
58
  const { z } = pi.zod;
57
59
  pi.setLabel("Devflow Platform Runtime");
60
+ let modelBeforeVisionTurn: Parameters<ExtensionAPI["setModel"]>[0] | undefined;
61
+ let visionTurnActive = false;
62
+
63
+ pi.on("input", async (event, ctx) => {
64
+ if (event.source !== "interactive" || !event.images?.length) return;
65
+
66
+ const configuredVision = options.getConfiguredRole?.("vision")?.trim();
67
+ if (!configuredVision) {
68
+ ctx.ui.notify("检测到图片,但尚未配置 VISION 模型。请先在 /model 中配置 VISION,再重新提交。", "warning");
69
+ if (ctx.hasUI) await ctx.ui.input("VISION 未配置", "请先在 /model 中配置 VISION,完成后重新提交本次图片");
70
+ return { handled: true };
71
+ }
72
+
73
+ const visionModel = ctx.models.resolve("@vision");
74
+ if (!visionModel || !visionModel.input.includes("image")) {
75
+ ctx.ui.notify(`VISION 配置无法解析为支持图片输入的模型:${configuredVision}`, "error");
76
+ if (ctx.hasUI) await ctx.ui.input("VISION 配置无效", "请选择支持图片输入的模型后重新提交");
77
+ return { handled: true };
78
+ }
79
+
80
+ modelBeforeVisionTurn = ctx.models.current();
81
+ if (!(await pi.setModel(visionModel))) {
82
+ modelBeforeVisionTurn = undefined;
83
+ ctx.ui.notify(`无法切换到 VISION 模型:${visionModel.provider}/${visionModel.id}`, "error");
84
+ return { handled: true };
85
+ }
86
+ visionTurnActive = true;
87
+ ctx.ui.notify(`本回合已切换到 VISION:${visionModel.provider}/${visionModel.id}`, "info");
88
+ return;
89
+ });
90
+
91
+ pi.on("turn_end", async (_event, ctx) => {
92
+ if (!visionTurnActive || !modelBeforeVisionTurn) return;
93
+ const previousModel = modelBeforeVisionTurn;
94
+ modelBeforeVisionTurn = undefined;
95
+ visionTurnActive = false;
96
+ await pi.setModel(previousModel);
97
+ ctx.ui.notify(`图片回合结束,已恢复模型:${previousModel.provider}/${previousModel.id}`, "info");
98
+ });
58
99
  const usageParameters = z.object({
59
100
  source: z.enum(["project", "omp"]).default("project"),
60
101
  session: z.string().optional(),
package/src/runtime.ts CHANGED
@@ -37,7 +37,9 @@ const MANAGED_SETTINGS: Record<string, unknown> = {
37
37
  // The interactive session is the high-capability Planner. Low-cost agents
38
38
  // are dispatched only through explicit Devflow steps, one at a time by
39
39
  // default, so background work cannot silently widen a user's request.
40
- "modelRoles.default": "@slow",
40
+ // Keep ordinary interactive turns on the user's DEFAULT role. In particular,
41
+ // do not inherit SLOW just because a thinking model is configured.
42
+ "defaultThinkingLevel": "low",
41
43
  "task.batch": false,
42
44
  "task.maxConcurrency": 1,
43
45
  "task.maxRecursionDepth": 1,
@@ -140,6 +142,7 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
140
142
  sessionRoot: sessionDir,
141
143
  ompSessionRoot: path.join(sharedAgentDir, "sessions"),
142
144
  projectId: project.manifest.projectId,
145
+ getConfiguredRole: role => settings.getModelRole(role),
143
146
  }),
144
147
  ],
145
148
  hasUI: options.hasUI ?? true,
@@ -8,6 +8,8 @@ This repository uses an OMP-native, project-scoped software delivery runtime.
8
8
  - Inspect workflow state with `devflow_state`; do not infer state from chat history.
9
9
  - Initialize delivery with `devflow_bootstrap`.
10
10
  - Mutate checkpoints and steps only with `devflow_transition`.
11
+ - To abandon an active feature, use the runtime-confirmed `devflow_transition`
12
+ `cancel-feature` operation or `/devflow abandon`; never edit control files.
11
13
  - Never directly edit `.planning/STATE.md`, `.planning/.next-intent`, `.planning/workflows/`, or `.planning/artifacts/index.yaml`.
12
14
  - The parent session is the only workflow-state writer. Task agents return evidence or patches and never advance state.
13
15
 
@@ -1,6 +1,9 @@
1
1
  # Devflow owns this project-local OMP profile. Model ids stay in the user's
2
2
  # global config; only role storage and development policy are project-scoped.
3
3
  modelRoleStorage: project
4
+ # Ordinary turns stay on DEFAULT and use only low automatic thinking. Agent
5
+ # frontmatter may still opt into medium/high for explicitly routed work.
6
+ defaultThinkingLevel: low
4
7
 
5
8
  # Do not import competing editor/harness configuration into this dedicated Pi.
6
9
  # These are discovery-source ids, not model-provider ids.