@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 +1 -1
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +54 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +29 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +22 -0
- package/src/constants.ts +1 -1
- package/src/platform-extension.ts +41 -0
- package/src/runtime.ts +4 -1
- package/templates/omp/AGENTS.md +2 -0
- package/templates/omp/config.yml +3 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
-
|
|
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,
|
package/templates/omp/AGENTS.md
CHANGED
|
@@ -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
|
|
package/templates/omp/config.yml
CHANGED
|
@@ -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.
|