@guanlin4924/devflow-pi 0.1.8 → 0.1.9
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 +20 -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/package.json
CHANGED
|
@@ -1550,6 +1550,26 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1550
1550
|
ctx.ui.notify(`Devflow advance failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
|
|
1551
1551
|
return;
|
|
1552
1552
|
}
|
|
1553
|
+
if (inspected.snapshot.action === "workflow_complete" && inspected.snapshot.source_flow) {
|
|
1554
|
+
const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
|
|
1555
|
+
if (revision === undefined) {
|
|
1556
|
+
ctx.ui.notify("补救分支缺少 revision,拒绝返回主流程。", "error");
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
1560
|
+
"--write",
|
|
1561
|
+
"--json",
|
|
1562
|
+
"--expected-revision",
|
|
1563
|
+
String(revision),
|
|
1564
|
+
"--exit-failure-branch",
|
|
1565
|
+
]);
|
|
1566
|
+
if (!result.ok) {
|
|
1567
|
+
ctx.ui.notify(`补救分支返回失败:${result.stderr || result.stdout}`, "error");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
ctx.ui.notify("补救分支已返回主流程;仍需审查 reverify 结果,并在 CP-4 明确确认后才能继续。", "info");
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1553
1573
|
if (inspected.snapshot.status === "waiting_checkpoint") {
|
|
1554
1574
|
ctx.ui.notify("当前状态等待 checkpoint;请使用 /devflow checkpoint。", "warning");
|
|
1555
1575
|
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
|