@guanlin4924/devflow-pi 0.1.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -47,7 +47,6 @@ const STATE_MUTATION_TOOLS = new Set([
47
47
  "devflow_bootstrap",
48
48
  "devflow_transition",
49
49
  "devflow_artifact",
50
- "devflow_recovery",
51
50
  "devflow_evidence",
52
51
  ]);
53
52
 
@@ -1551,6 +1550,26 @@ export default function devflowCore(pi: ExtensionAPI) {
1551
1550
  ctx.ui.notify(`Devflow advance failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1552
1551
  return;
1553
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
+ }
1554
1573
  if (inspected.snapshot.status === "waiting_checkpoint") {
1555
1574
  ctx.ui.notify("当前状态等待 checkpoint;请使用 /devflow checkpoint。", "warning");
1556
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["current_step"] = step_id
417
- state["status"] = "completed"
418
- state.delete("checkpoint")
419
- state.delete("checkpoint_label")
420
- action_description = "Step completed: #{step["label"]} (#{step_id}). All steps complete."
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(revision: Integer(state.fetch("revision", 0)))
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
- candidate = build_registry(root, workflow, state)
67
- 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)
68
- current = Array(workflow["steps"]).find { |step| step["id"] == state["current_step"] }
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
- registry_path = File.join(root, workflow.dig("artifact_registry", "source"))
84
- DevflowControlIO.atomic_write(registry_path, candidate.to_yaml)
85
- result = { "ok" => true, "classification" => "safe_repairable", "written" => true, "revision" => state.fetch("revision", 0), "plan" => { "state_transition" => "none", "registry_write" => true } }
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
@@ -27,6 +39,7 @@ state_contract:
27
39
  pending_steps:
28
40
  - diagnose
29
41
  - execute-fix
42
+ - code-index-refresh
30
43
  - reverify
31
44
  - lesson
32
45
 
package/src/runtime.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { existsSync } from "node:fs";
2
1
  import { mkdir } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
 
@@ -79,13 +78,12 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
79
78
  utils.logger.setTransports({ file: path.join(productHome, "logs") });
80
79
  const omp = await import("@oh-my-pi/pi-coding-agent");
81
80
  const roots = pluginRoots();
82
- const configFiles = [path.join(sharedAgentDir, "config.yml"), path.join(sharedAgentDir, "config.yaml")].filter(
83
- candidate => existsSync(candidate),
84
- );
81
+ // Reuse credentials and model discovery from the user's OMP agent, but not
82
+ // its config.yml: a global modelRoles block would override choices made in
83
+ // Devflow's own Roles screen after every restart.
85
84
  const settings = await omp.Settings.init({
86
85
  cwd: project.root,
87
86
  agentDir: productAgentDir,
88
- configFiles,
89
87
  overrides: {
90
88
  ...MANAGED_SETTINGS,
91
89
  // OMP 17.2.7 classifies extension-package skills behind the generic