@guanlin4924/devflow-pi 0.1.26 → 0.1.28

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.28
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.28",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -12,7 +12,8 @@
12
12
  "doctor": "bun run src/cli.ts doctor",
13
13
  "test:devflow-pi": "bun test tests/devflow-pi",
14
14
  "test": "bun test tests/devflow-pi && python3 -m unittest discover -s tests -p 'test_*.py'",
15
- "check": "bun test tests/devflow-pi && python3 -m unittest discover -s tests -p 'test_*.py'"
15
+ "check": "bun test tests/devflow-pi && python3 -m unittest discover -s tests -p 'test_*.py'",
16
+ "release": "bash scripts/release-npm.sh"
16
17
  },
17
18
  "engines": {
18
19
  "bun": ">=1.3.14"
@@ -1599,7 +1599,13 @@ export default function devflowCore(pi: ExtensionAPI) {
1599
1599
  ) {
1600
1600
  return { block: true, reason: planningToolBlockReason(toolName) };
1601
1601
  }
1602
- if (isStateMutationTool(toolName)) {
1602
+ const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
1603
+ // Registry reconciliation is intentionally the one write operation an
1604
+ // agent may request while a step is blocked: it changes only derived
1605
+ // artifact metadata and never advances STATE, checkpoints, or ownership.
1606
+ const isRegistryReconciliation =
1607
+ toolName === "devflow_artifact" && toolInput?.action === "sync" && toolInput?.preserve === false;
1608
+ if (isStateMutationTool(toolName) && !isRegistryReconciliation) {
1603
1609
  return {
1604
1610
  block: true,
1605
1611
  reason:
@@ -2403,9 +2409,11 @@ export default function devflowCore(pi: ExtensionAPI) {
2403
2409
  label: "Devflow Transition",
2404
2410
  description: "Apply one validated delivery-state transition. This is the only supported STATE.md mutation path.",
2405
2411
  parameters: z.object({
2406
- action: z.enum(["complete-step", "confirm-checkpoint", "checkpoint-decision", "enter-failure-branch"]),
2412
+ action: z.enum(["complete-step", "confirm-checkpoint", "checkpoint-decision", "enter-failure-branch", "cancel-feature"]),
2407
2413
  step: z.string().optional(),
2408
2414
  decision: z.string().optional(),
2415
+ reason: z.string().optional(),
2416
+ confirm: z.literal(true).optional(),
2409
2417
  state: z.string().optional(),
2410
2418
  expectedRevision: z.number().int().min(0),
2411
2419
  }),
@@ -2438,6 +2446,19 @@ export default function devflowCore(pi: ExtensionAPI) {
2438
2446
  return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "decision is required" });
2439
2447
  }
2440
2448
  args.push("--checkpoint-decision", params.decision);
2449
+ } else if (params.action === "cancel-feature") {
2450
+ const inspected = await inspectWorkflow(ctx.cwd);
2451
+ const snapshot = inspected.snapshot;
2452
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : "当前需求";
2453
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
2454
+ if (!inspected.result.ok || revision === undefined) return renderResult(inspected.result, { action: params.action });
2455
+ if (!params.reason?.trim()) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "reason is required" });
2456
+ const confirmed = await ctx.ui.confirm(
2457
+ "确认弃用当前需求",
2458
+ `需求:${feature}\n原因:${params.reason.trim()}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
2459
+ );
2460
+ if (!confirmed) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "User declined feature cancellation." });
2461
+ args.push("--cancel", "--cancel-reason", params.reason.trim());
2441
2462
  } else {
2442
2463
  args.push("--enter-failure-branch");
2443
2464
  }
@@ -2601,6 +2622,42 @@ export default function devflowCore(pi: ExtensionAPI) {
2601
2622
  ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
2602
2623
  return;
2603
2624
  }
2625
+ if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
2626
+ const inspected = await inspectWorkflow(ctx.cwd);
2627
+ const snapshot = inspected.snapshot;
2628
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
2629
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
2630
+ const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
2631
+ if (!inspected.result.ok || revision === undefined || !feature) {
2632
+ ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
2633
+ return;
2634
+ }
2635
+ if (!status || ["completed", "cancelled", "failed"].includes(status)) {
2636
+ ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
2637
+ return;
2638
+ }
2639
+ const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
2640
+ const confirmed = await ctx.ui.confirm(
2641
+ "确认弃用当前需求",
2642
+ `需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
2643
+ );
2644
+ if (!confirmed) {
2645
+ ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
2646
+ return;
2647
+ }
2648
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
2649
+ "--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
2650
+ ]);
2651
+ if (!result.ok) {
2652
+ ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
2653
+ return;
2654
+ }
2655
+ activeContract = undefined;
2656
+ contractAgentCalls = 0;
2657
+ setControlState(ctx, initialControlState());
2658
+ ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
2659
+ return;
2660
+ }
2604
2661
  if (trimmed === "project-init") {
2605
2662
  const initialization = await collectProjectInitialization(ctx);
2606
2663
  if (!initialization) return;
@@ -2672,7 +2729,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2672
2729
  }
2673
2730
  const existing = await inspectWorkflow(ctx.cwd);
2674
2731
  const existingFeature = existing.snapshot?.current_feature;
2675
- const previousWorkflowComplete = existing.snapshot?.action === "workflow_complete";
2732
+ const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
2676
2733
  if (
2677
2734
  !previousWorkflowComplete &&
2678
2735
  typeof existingFeature === "string" &&
@@ -2753,7 +2810,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2753
2810
  return;
2754
2811
  }
2755
2812
  ctx.ui.notify(
2756
- "Usage: /devflow doctor | status | explain-state | recover | project-init | start <feature>|<phase> | resume",
2813
+ "Usage: /devflow doctor | status | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
2757
2814
  "warning",
2758
2815
  );
2759
2816
  },
@@ -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.28";
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.