@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 +1 -1
- package/package.json +3 -2
- package/plugins/devflow-core/extensions/devflow-core.ts +61 -4
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guanlin4924/devflow-pi",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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.
|