@guanlin4924/devflow-pi 0.1.8 → 0.1.10
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/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +21 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +70 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +24 -19
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/uat-remediation.yaml +12 -0
- package/plugins/devflow-implementation/agents/implementation-worker.md +5 -3
- package/plugins/devflow-intel/agents/project-scout.md +12 -11
- package/src/runtime.ts +6 -0
package/package.json
CHANGED
|
@@ -108,6 +108,7 @@ const DEVFLOW_CHINESE_LANGUAGE_POLICY = `
|
|
|
108
108
|
Devflow 语言策略(强制):
|
|
109
109
|
- 默认使用简体中文与用户沟通。
|
|
110
110
|
- 需求、计划、Evidence Pack、Execution Contract、审查报告、验证报告和阻塞原因必须使用简体中文。
|
|
111
|
+
- 对用户可见的思考、推理摘要和阶段性进度必须使用简体中文;不要展示英文推理草稿来代替中文结论。
|
|
111
112
|
- 文件名、类名、函数名、接口路径、命令、代码、日志原文和模型/Agent 名称保持原样,不翻译标识符。
|
|
112
113
|
- 推理时先区分“事实、推断、未知、待用户确认”,不要用英文长篇解释替代证据。
|
|
113
114
|
- 如果引用英文源码或工具输出,保留原文并紧跟中文解释。
|
|
@@ -1550,6 +1551,26 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1550
1551
|
ctx.ui.notify(`Devflow advance failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
|
|
1551
1552
|
return;
|
|
1552
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
|
+
}
|
|
1553
1574
|
if (inspected.snapshot.status === "waiting_checkpoint") {
|
|
1554
1575
|
ctx.ui.notify("当前状态等待 checkpoint;请使用 /devflow checkpoint。", "warning");
|
|
1555
1576
|
return;
|
|
@@ -16,6 +16,7 @@ options = {
|
|
|
16
16
|
confirm_checkpoint: false,
|
|
17
17
|
checkpoint_decision: nil,
|
|
18
18
|
enter_failure_branch: false,
|
|
19
|
+
exit_failure_branch: false,
|
|
19
20
|
expected_revision: nil,
|
|
20
21
|
json: false
|
|
21
22
|
}
|
|
@@ -28,6 +29,7 @@ OptionParser.new do |opts|
|
|
|
28
29
|
opts.on("--confirm-checkpoint", "Advance from waiting checkpoint to the next pending step") { options[:confirm_checkpoint] = true }
|
|
29
30
|
opts.on("--checkpoint-decision VALUE", "Set checkpoint decision, e.g. approved, rejected, needs_fix") { |value| options[:checkpoint_decision] = value }
|
|
30
31
|
opts.on("--enter-failure-branch", "Transition into the configured failure branch for the current step") { options[:enter_failure_branch] = true }
|
|
32
|
+
opts.on("--exit-failure-branch", "Return from a completed failure branch to its configured source flow") { options[:exit_failure_branch] = true }
|
|
31
33
|
opts.on("--expected-revision REVISION", Integer, "Reject a stale write unless STATE.md has this revision") { |value| options[:expected_revision] = value }
|
|
32
34
|
opts.on("--json", "Print JSON output") { options[:json] = true }
|
|
33
35
|
end.parse!
|
|
@@ -337,6 +339,46 @@ def fail_result(reason, details = {})
|
|
|
337
339
|
{ "ok" => false, "reason" => reason, "details" => details }
|
|
338
340
|
end
|
|
339
341
|
|
|
342
|
+
# Return from a completed failure branch using its declared return_to contract.
|
|
343
|
+
# The caller must still explicitly confirm the returned checkpoint before any
|
|
344
|
+
# subsequent source-flow step can proceed.
|
|
345
|
+
def apply_return_to!(root, state, branch_workflow)
|
|
346
|
+
return_to = branch_workflow["return_to"]
|
|
347
|
+
source_flow = state["source_flow"]
|
|
348
|
+
source_workflow_config = state["source_workflow_config"]
|
|
349
|
+
return nil unless return_to && source_flow && source_workflow_config
|
|
350
|
+
|
|
351
|
+
main_workflow_path = File.join(root, source_workflow_config)
|
|
352
|
+
return nil unless File.exist?(main_workflow_path)
|
|
353
|
+
|
|
354
|
+
main_workflow = load_yaml_file(main_workflow_path)
|
|
355
|
+
main_steps = Array(main_workflow["steps"])
|
|
356
|
+
target_step_id = return_to["step"]
|
|
357
|
+
return nil unless main_steps.any? { |step| step.fetch("id") == target_step_id }
|
|
358
|
+
|
|
359
|
+
ordered_ids = main_steps.map { |step| step.fetch("id") }
|
|
360
|
+
idx = ordered_ids.index(target_step_id)
|
|
361
|
+
state["current_flow"] = return_to["flow"] || source_flow
|
|
362
|
+
state["workflow_config"] = source_workflow_config
|
|
363
|
+
state["current_step"] = target_step_id
|
|
364
|
+
state["status"] = return_to["status"] || "waiting_checkpoint"
|
|
365
|
+
state["completed_steps"] = ordered_ids[0..idx]
|
|
366
|
+
state["pending_steps"] = ordered_ids[(idx + 1)..-1] || []
|
|
367
|
+
state["last_completed_step"] = target_step_id
|
|
368
|
+
if return_to["checkpoint"]
|
|
369
|
+
state["checkpoint"] = return_to["checkpoint"]
|
|
370
|
+
state["checkpoint_label"] = checkpoint_label(main_workflow, return_to["checkpoint"])
|
|
371
|
+
else
|
|
372
|
+
state.delete("checkpoint")
|
|
373
|
+
state.delete("checkpoint_label")
|
|
374
|
+
end
|
|
375
|
+
state.delete("checkpoint_decision")
|
|
376
|
+
state.delete("source_flow")
|
|
377
|
+
state.delete("source_workflow_config")
|
|
378
|
+
"Failure branch completed. Returned to #{target_step_id} (#{state["current_flow"]})" \
|
|
379
|
+
"#{return_to["checkpoint"] ? ", waiting at #{return_to["checkpoint"]}" : ""}."
|
|
380
|
+
end
|
|
381
|
+
|
|
340
382
|
project_lock = DevflowControlIO.acquire_project_lock(root) if options[:write]
|
|
341
383
|
|
|
342
384
|
begin
|
|
@@ -413,11 +455,16 @@ result =
|
|
|
413
455
|
next_step = step_by_id[next_id]
|
|
414
456
|
action_description = "Step completed: #{step["label"]} (#{step_id}). Advanced to #{next_step ? next_step["label"] : next_id}."
|
|
415
457
|
else
|
|
416
|
-
state["
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
458
|
+
return_description = state["source_flow"] ? apply_return_to!(root, state, workflow) : nil
|
|
459
|
+
if return_description
|
|
460
|
+
action_description = "Step completed: #{step["label"]} (#{step_id}). #{return_description}"
|
|
461
|
+
else
|
|
462
|
+
state["current_step"] = step_id
|
|
463
|
+
state["status"] = "completed"
|
|
464
|
+
state.delete("checkpoint")
|
|
465
|
+
state.delete("checkpoint_label")
|
|
466
|
+
action_description = "Step completed: #{step["label"]} (#{step_id}). All steps complete."
|
|
467
|
+
end
|
|
421
468
|
end
|
|
422
469
|
|
|
423
470
|
changed = true
|
|
@@ -483,10 +530,28 @@ result =
|
|
|
483
530
|
end
|
|
484
531
|
end
|
|
485
532
|
|
|
533
|
+
if result.nil? && options[:exit_failure_branch]
|
|
534
|
+
if state["source_flow"].nil? || state["source_workflow_config"].nil?
|
|
535
|
+
result = fail_result("not_in_failure_branch")
|
|
536
|
+
elsif state["status"] != "completed" || Array(state["pending_steps"]).any?
|
|
537
|
+
result = fail_result("failure_branch_incomplete", "status" => state["status"], "pending_steps" => state["pending_steps"])
|
|
538
|
+
else
|
|
539
|
+
return_description = apply_return_to!(root, state, workflow)
|
|
540
|
+
if return_description.nil?
|
|
541
|
+
result = fail_result("missing_return_to", "workflow" => state["current_flow"])
|
|
542
|
+
else
|
|
543
|
+
changed = true
|
|
544
|
+
actions << "failure_branch_exited"
|
|
545
|
+
action_description = return_description
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
486
550
|
if result.nil?
|
|
487
551
|
if changed
|
|
488
552
|
state["state_updated_at"] = Date.today.to_s
|
|
489
553
|
state["revision"] = current_revision + 1
|
|
554
|
+
workflow_path, workflow = load_workflow(root, state)
|
|
490
555
|
end
|
|
491
556
|
# Build the registry from the candidate state before validating it. A
|
|
492
557
|
# step's output is normally written before this transition, so the
|
|
@@ -583,7 +583,11 @@ result =
|
|
|
583
583
|
end
|
|
584
584
|
|
|
585
585
|
if defined?(state) && state.is_a?(Hash) && result.is_a?(Hash)
|
|
586
|
-
result = result.merge(
|
|
586
|
+
result = result.merge(
|
|
587
|
+
revision: Integer(state.fetch("revision", 0)),
|
|
588
|
+
source_flow: state["source_flow"],
|
|
589
|
+
source_workflow_config: state["source_workflow_config"]
|
|
590
|
+
)
|
|
587
591
|
end
|
|
588
592
|
|
|
589
593
|
if options[:json]
|
|
@@ -63,26 +63,31 @@ begin
|
|
|
63
63
|
state = YAML.safe_load(front_matter(File.read(state_path)), permitted_classes: [], aliases: false)
|
|
64
64
|
workflow_path = File.expand_path(state.fetch("workflow_config"), root)
|
|
65
65
|
workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
missing_current_outputs = Array(current && current["outputs"]).map do |output|
|
|
70
|
-
next unless output["required"]
|
|
71
|
-
path = interpolate_path(output.fetch("path"), state)
|
|
72
|
-
path unless File.exist?(File.join(root, path))
|
|
73
|
-
end.compact
|
|
74
|
-
if missing_current_outputs.any?
|
|
75
|
-
result = { "ok" => false, "classification" => "unsafe", "reason" => "missing_required_output", "missing_outputs" => missing_current_outputs, "plan" => { "state_transition" => "none", "registry_write" => false } }
|
|
76
|
-
elsif !validation["ok"]
|
|
77
|
-
result = { "ok" => false, "classification" => "unsafe", "reason" => "ambiguous_or_invalid_state", "issues" => validation["errors"], "plan" => { "state_transition" => "none", "registry_write" => false } }
|
|
78
|
-
elsif !options[:confirm] || !options[:write]
|
|
79
|
-
result = { "ok" => false, "classification" => "safe_repairable", "reason" => "confirmation_required", "revision" => state.fetch("revision", 0), "plan" => { "state_transition" => "none", "registry_write" => true, "registry" => candidate } }
|
|
80
|
-
elsif options[:expected_revision] != state.fetch("revision", 0)
|
|
81
|
-
result = { "ok" => false, "classification" => "unsafe", "reason" => "stale_revision", "expected_revision" => options[:expected_revision], "actual_revision" => state.fetch("revision", 0) }
|
|
66
|
+
registry_source = workflow.dig("artifact_registry", "source")
|
|
67
|
+
if !registry_source.is_a?(String) || registry_source.empty?
|
|
68
|
+
result = { "ok" => false, "classification" => "unsafe", "reason" => "missing_artifact_registry_config", "plan" => { "state_transition" => "none", "registry_write" => false } }
|
|
82
69
|
else
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
70
|
+
candidate = build_registry(root, workflow, state)
|
|
71
|
+
validation = WorkflowStateValidator.validate(root: root, state_path: state_path, state_override: state, workflow_override: workflow, artifact_registry_override: candidate, strict_artifact_status: true, strict_registry_revision: true)
|
|
72
|
+
current = Array(workflow["steps"]).find { |step| step["id"] == state["current_step"] }
|
|
73
|
+
missing_current_outputs = Array(current && current["outputs"]).map do |output|
|
|
74
|
+
next unless output["required"]
|
|
75
|
+
path = interpolate_path(output.fetch("path"), state)
|
|
76
|
+
path unless File.exist?(File.join(root, path))
|
|
77
|
+
end.compact
|
|
78
|
+
if missing_current_outputs.any?
|
|
79
|
+
result = { "ok" => false, "classification" => "unsafe", "reason" => "missing_required_output", "missing_outputs" => missing_current_outputs, "plan" => { "state_transition" => "none", "registry_write" => false } }
|
|
80
|
+
elsif !validation["ok"]
|
|
81
|
+
result = { "ok" => false, "classification" => "unsafe", "reason" => "ambiguous_or_invalid_state", "issues" => validation["errors"], "plan" => { "state_transition" => "none", "registry_write" => false } }
|
|
82
|
+
elsif !options[:confirm] || !options[:write]
|
|
83
|
+
result = { "ok" => false, "classification" => "safe_repairable", "reason" => "confirmation_required", "revision" => state.fetch("revision", 0), "plan" => { "state_transition" => "none", "registry_write" => true, "registry" => candidate } }
|
|
84
|
+
elsif options[:expected_revision] != state.fetch("revision", 0)
|
|
85
|
+
result = { "ok" => false, "classification" => "unsafe", "reason" => "stale_revision", "expected_revision" => options[:expected_revision], "actual_revision" => state.fetch("revision", 0) }
|
|
86
|
+
else
|
|
87
|
+
registry_path = File.join(root, registry_source)
|
|
88
|
+
DevflowControlIO.atomic_write(registry_path, candidate.to_yaml)
|
|
89
|
+
result = { "ok" => true, "classification" => "safe_repairable", "written" => true, "revision" => state.fetch("revision", 0), "plan" => { "state_transition" => "none", "registry_write" => true } }
|
|
90
|
+
end
|
|
86
91
|
end
|
|
87
92
|
end
|
|
88
93
|
ensure
|
|
@@ -4,6 +4,18 @@ name: Verify Failure Remediation
|
|
|
4
4
|
description: >
|
|
5
5
|
Generic remediation flow for failed verification or rejected acceptance checkpoints.
|
|
6
6
|
|
|
7
|
+
artifact_registry:
|
|
8
|
+
source: ".planning/artifacts/index.yaml"
|
|
9
|
+
path_key: path
|
|
10
|
+
status_key: status
|
|
11
|
+
pending_statuses:
|
|
12
|
+
- planned
|
|
13
|
+
- pending_rerun
|
|
14
|
+
terminal_success_statuses:
|
|
15
|
+
- approved
|
|
16
|
+
- completed
|
|
17
|
+
- passed
|
|
18
|
+
|
|
7
19
|
entry:
|
|
8
20
|
from_flow: feature-development
|
|
9
21
|
from_step: verify
|
|
@@ -10,11 +10,13 @@ tools:
|
|
|
10
10
|
- edit
|
|
11
11
|
- write
|
|
12
12
|
model: "@task"
|
|
13
|
-
thinkingLevel:
|
|
13
|
+
thinkingLevel: medium
|
|
14
14
|
autoloadSkills:
|
|
15
15
|
- workflow-execute-phase
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
Implement only the assigned plan slice.
|
|
18
|
+
Implement only the assigned plan slice. Do not re-plan the feature, compare alternatives, or explore unrelated modules. Read only the approved files, their immediate dependencies, and the focused tests needed for this slice. Preserve unrelated work and report deviations instead of silently expanding scope.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
If the execution contract is missing, ambiguous, or contradicted by the code, stop before editing and return `STATUS: escalate` with the conflicting evidence. Run the smallest focused validation once; report failures rather than repeatedly trying speculative fixes.
|
|
21
|
+
|
|
22
|
+
Return `STATUS`, changed files, validation evidence, and one escalation/blocker if needed. Never edit `.planning/STATE.md`, `.planning/artifacts/index.yaml`, workflow definitions, or checkpoint decisions; the parent session owns those through Devflow tools.
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-scout
|
|
3
|
-
description:
|
|
3
|
+
description: Low-cost read-only evidence collector for bounded code-location questions; escalates when the question needs architecture or product judgment.
|
|
4
4
|
tools:
|
|
5
5
|
- read
|
|
6
6
|
- grep
|
|
7
7
|
- find
|
|
8
|
-
- lsp
|
|
9
8
|
- devflow_code_index
|
|
10
9
|
model: "@smol"
|
|
11
|
-
thinkingLevel:
|
|
10
|
+
thinkingLevel: low
|
|
12
11
|
autoloadSkills:
|
|
13
12
|
- code-index-query
|
|
14
|
-
- workflow-code-index
|
|
15
13
|
---
|
|
16
14
|
|
|
17
|
-
Act as a read-only evidence collector
|
|
15
|
+
Act as a bounded read-only evidence collector, not a planner. Use the code index first, then make at most three targeted reads or searches needed to answer the assigned question. Do not inspect adjacent systems merely to be thorough.
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
If the answer needs architecture tradeoffs, requirements interpretation, a cross-module decision, or evidence remains insufficient after the bounded search, stop and return `STATUS: escalate`. Do not guess or continue exploring.
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
Return exactly:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
STATUS: done | escalate | blocked
|
|
23
|
+
EVIDENCE: up to 5 path/symbol facts
|
|
24
|
+
ANSWER: concise answer or smallest affected-file set
|
|
25
|
+
NEXT: only the required escalation or blocker; otherwise none
|
|
26
|
+
```
|
|
26
27
|
|
|
27
28
|
Do not edit files, mutate workflow state, or widen the task beyond the supplied question.
|
package/src/runtime.ts
CHANGED
|
@@ -35,6 +35,12 @@ const MANAGED_SETTINGS: Record<string, unknown> = {
|
|
|
35
35
|
"task.maxConcurrency": 1,
|
|
36
36
|
"task.maxRecursionDepth": 1,
|
|
37
37
|
"task.enableLsp": true,
|
|
38
|
+
// Keep cheap workers bounded by role prompts. OMP's loop guards remain
|
|
39
|
+
// enabled, while these switches prevent accidental high-effort keywords from
|
|
40
|
+
// widening a normal Devflow turn into an unbounded orchestration request.
|
|
41
|
+
"magicKeywords.ultrathink": false,
|
|
42
|
+
"magicKeywords.orchestrate": false,
|
|
43
|
+
"magicKeywords.workflow": false,
|
|
38
44
|
"task.agentModelOverrides": {
|
|
39
45
|
"project-scout": "@smol",
|
|
40
46
|
"requirements-auditor": "@slow",
|