@guanlin4924/devflow-pi 0.1.105 → 0.1.107

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.
Files changed (66) hide show
  1. package/README.md +10 -8
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +287 -394
  4. package/plugins/devflow-core/runtime/hooks/context_sync_gate.py +3 -1
  5. package/plugins/devflow-core/runtime/hooks/hook_common.py +6 -5
  6. package/plugins/devflow-core/runtime/hooks/index_gate.py +19 -26
  7. package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +2 -0
  8. package/plugins/devflow-core/skills/wireframe-render/SKILL.md +2 -0
  9. package/plugins/devflow-core/skills/wireframe-review/SKILL.md +2 -0
  10. package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +2 -0
  11. package/plugins/devflow-core/skills/workflow-dynamic/SKILL.md +41 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +37 -50
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +46 -191
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/artifact_specs.rb +12 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dynamic_plan.rb +504 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +38 -1
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_policy.rb +11 -0
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_result.rb +12 -0
  19. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +4 -9
  20. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +19 -86
  21. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +24 -13
  22. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +4 -4
  23. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +7 -1
  24. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +17 -39
  25. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +39 -3
  26. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +2 -3
  27. package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +0 -1
  28. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +13 -5
  29. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +1 -1
  30. package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +2 -0
  31. package/plugins/devflow-design/skills/workflow-api-call-guide/SKILL.md +2 -0
  32. package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +3 -1
  33. package/plugins/devflow-design/skills/workflow-api-supplement/SKILL.md +2 -0
  34. package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +2 -0
  35. package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +6 -4
  36. package/plugins/devflow-design/skills/workflow-design-rule-pack/SKILL.md +2 -0
  37. package/plugins/devflow-design/skills/workflow-feature-breakdown/SKILL.md +2 -0
  38. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +6 -4
  39. package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +3 -1
  40. package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +4 -2
  41. package/plugins/devflow-implementation/agents/implementation-worker.md +2 -8
  42. package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +5 -0
  43. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +3 -1
  44. package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +6 -1
  45. package/plugins/devflow-intel/skills/code-index-query/SKILL.md +2 -0
  46. package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +2 -0
  47. package/plugins/devflow-intel/skills/workflow-context-sync/SKILL.md +2 -0
  48. package/plugins/devflow-intel/skills/workflow-update-navigation/SKILL.md +3 -1
  49. package/plugins/devflow-quality/agents/code-reviewer.md +2 -0
  50. package/plugins/devflow-quality/skills/code-review/SKILL.md +7 -2
  51. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -1
  52. package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +2 -0
  53. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +2 -0
  54. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +2 -0
  55. package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +2 -0
  56. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +7 -5
  57. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +11 -6
  58. package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +2 -0
  59. package/plugins/devflow-requirements/skills/workflow-demand-clarify/SKILL.md +2 -0
  60. package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +2 -0
  61. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +6 -2
  62. package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +5 -0
  63. package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +2 -0
  64. package/src/constants.ts +1 -1
  65. package/src/dynamic-workflow.ts +31 -0
  66. package/templates/omp/AGENTS.md +8 -3
@@ -6,6 +6,8 @@ require "optparse"
6
6
  require "yaml"
7
7
  require_relative "validate_state"
8
8
  require_relative "checkpoint_meta"
9
+ require_relative "gate_policy"
10
+ require_relative "dynamic_plan"
9
11
 
10
12
  options = { json: false, state: nil }
11
13
  OptionParser.new do |opts|
@@ -15,6 +17,11 @@ OptionParser.new do |opts|
15
17
  end.parse!
16
18
 
17
19
  root = Dir.pwd
20
+ dynamic_state = DynamicPlan.read_state(root)
21
+ if dynamic_state && dynamic_state["current_flow"] == "dynamic"
22
+ puts JSON.pretty_generate(DynamicPlan.snapshot(root, dynamic_state))
23
+ exit 0
24
+ end
18
25
  state_path = options[:state] ? File.expand_path(options[:state], root) : File.join(root, ".devflow/planning", "STATE.md")
19
26
 
20
27
  def fail_result(reason, details = {})
@@ -192,19 +199,7 @@ def track_allowed_steps(track)
192
199
  end
193
200
 
194
201
  def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
195
- track = active_track(state)
196
- unless track
197
- return pending_ids.find do |step_id|
198
- step_by_id[step_id] && dependencies_satisfied?(step_by_id[step_id], completed_ids)
199
- end
200
- end
201
-
202
- allowed = track_allowed_steps(track)
203
- pending_ids.find do |step_id|
204
- allowed.include?(step_id) &&
205
- step_by_id[step_id] &&
206
- dependencies_satisfied?(step_by_id[step_id], completed_ids)
207
- end
202
+ pending_ids.find { |id| step_by_id[id] && dependencies_satisfied?(step_by_id[id], completed_ids) } || pending_ids.find { |id| step_by_id[id] }
208
203
  end
209
204
 
210
205
  def track_boundary(state, pending_ids, completed_ids)
@@ -366,27 +361,12 @@ result =
366
361
 
367
362
  if missing.any?
368
363
  fail_result("missing_state_fields", fields: missing)
369
- elsif project_context_missing?(root)
370
- project_context_result(
371
- "incomplete_project_context",
372
- missing: [
373
- ".devflow/planning/PROJECT.md",
374
- ".devflow/planning/navigation/INDEX.md",
375
- ".devflow/context/spec/index.md"
376
- ]
377
- )
378
364
  else
379
365
  pending_template_blockers = Array(state["blockers"]).select do |blocker|
380
366
  blocker.is_a?(Hash) && blocker["type"] == "PENDING_TEMPLATE"
381
367
  end
382
368
 
383
- if pending_template_blockers.any?
384
- fail_result(
385
- "pending_depth_spec_templates",
386
- blockers: pending_template_blockers,
387
- recovery: "add the missing template or mark the topic NOT_APPLICABLE, then rerun workflow-project-context"
388
- )
389
- else
369
+ begin
390
370
  workflow_path = state["workflow_config"] ||
391
371
  File.join(".devflow/planning", "workflows", "#{state["current_flow"]}.yaml")
392
372
  abs_workflow_path = File.join(root, workflow_path)
@@ -413,6 +393,8 @@ result =
413
393
  strict_artifact_status: true
414
394
  )
415
395
 
396
+ validation["warnings"] << GatePolicy.advisory({ "code" => "incomplete_project_context", "message" => "project context indexes are missing" }) if project_context_missing?(root)
397
+ validation["warnings"].concat(pending_template_blockers.map { |entry| GatePolicy.advisory(entry) })
416
398
  if !validation["ok"]
417
399
  {
418
400
  current_flow: state["current_flow"],
@@ -429,6 +411,8 @@ result =
429
411
  },
430
412
  next_step: nil,
431
413
  workflow_config: workflow_path,
414
+ warnings: Array(state["gate_advisories"]) + Array(validation["warnings"]),
415
+ gate_mode: "soft",
432
416
  artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
433
417
  "path" => artifact_registry_path,
434
418
  "error" => artifact_registry_error,
@@ -446,41 +430,6 @@ result =
446
430
  },
447
431
  recovery: "Fix missing/invalid artifacts or call devflow_artifact with action=sync and preserve=false, then call devflow_state with action=inspect."
448
432
  }
449
- elsif (drift = validation["warnings"].find { |issue| issue["code"] == "current_output_registry_drift" })
450
- {
451
- current_flow: state["current_flow"],
452
- current_feature: state["current_feature"],
453
- current_phase: state["current_phase"],
454
- status: state["status"],
455
- completed_steps: completed_ids,
456
- pending_steps: pending_ids,
457
- current_step: current_step && {
458
- id: current_step["id"],
459
- label: current_step["label"],
460
- skill: current_step["skill"],
461
- action: current_step["action"]
462
- },
463
- next_step: nil,
464
- workflow_config: workflow_path,
465
- artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
466
- "path" => artifact_registry_path,
467
- "error" => artifact_registry_error,
468
- "current_outputs_registered" => 0
469
- ),
470
- action: "recover_artifact_registry",
471
- reason: "current_output_registry_drift",
472
- drift: drift,
473
- recovery_plan: {
474
- classification: "safe_repairable",
475
- state_transition: "none",
476
- registry_write: true,
477
- confirmation_required: true,
478
- expected_revision: state.fetch("revision", 0),
479
- diff: [{ path: drift["path"], registry_exists: drift["registry_exists"], filesystem_exists: true,
480
- registry_content_valid: drift["registry_content_valid"], filesystem_content_valid: true }]
481
- },
482
- recovery: "After reviewing this diff, call devflow_recovery(expectedRevision=#{state.fetch("revision", 0)}, confirm=true). It only reconciles registry metadata; explicitly complete the step separately when ready."
483
- }
484
433
  elsif current_step.nil?
485
434
  fallback_next_id = next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
486
435
  fallback_next_step = fallback_next_id && step_by_id[fallback_next_id]
@@ -604,6 +553,8 @@ result =
604
553
  },
605
554
  parallel_window: parallel_window,
606
555
  workflow_config: workflow_path,
556
+ warnings: Array(state["gate_advisories"]) + Array(validation["warnings"]),
557
+ gate_mode: "soft",
607
558
  artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
608
559
  "path" => artifact_registry_path,
609
560
  "error" => artifact_registry_error,
@@ -614,26 +565,7 @@ result =
614
565
 
615
566
  boundary = track_boundary(state, pending_ids, completed_ids)
616
567
 
617
- if artifact_registry_error
618
- base.merge(
619
- action: "blocked",
620
- reason: artifact_registry_error,
621
- recovery: "Call devflow_artifact with action=sync."
622
- )
623
- elsif boundary
624
- base.merge(
625
- action: "blocked",
626
- reason: boundary["reason"],
627
- message: boundary["message"],
628
- active_track: active_track(state)
629
- )
630
- elsif failure_branch
631
- base.merge(
632
- action: "route_failure_branch",
633
- reason: "failure_branch_triggered",
634
- failure_branch: failure_branch
635
- )
636
- elsif state["status"] == "waiting_checkpoint"
568
+ if state["status"] == "waiting_checkpoint"
637
569
  missing_outputs = current_outputs
638
570
  .select { |output| output["required"] && !output["exists"] }
639
571
  .map { |output| output["resolved_path"] }
@@ -647,8 +579,9 @@ result =
647
579
  end
648
580
  missing_outputs.concat(checkpoint_meta_errors.map { |entry| entry["path"] }.compact)
649
581
  base.merge(
650
- action: missing_outputs.empty? ? "await_user_confirmation" : "blocked",
651
- reason: missing_outputs.empty? ? nil : "missing_checkpoint_outputs",
582
+ action: "await_user_confirmation",
583
+ reason: nil,
584
+ warnings: Array(base[:warnings]) + (missing_outputs.empty? ? [] : [{ code: "missing_checkpoint_outputs", paths: missing_outputs, soft_gate: true }]),
652
585
  missing_outputs: missing_outputs,
653
586
  checkpoint_meta_errors: checkpoint_meta_errors,
654
587
  resume_behavior: checkpoint && checkpoint["resume_behavior"],
@@ -15,6 +15,8 @@ require "optparse"
15
15
  require "time"
16
16
  require "yaml"
17
17
  require_relative "control_io"
18
+ require_relative "artifact_specs"
19
+ require_relative "gate_policy"
18
20
  require_relative "parallel_run"
19
21
 
20
22
  options = {
@@ -54,9 +56,8 @@ def interpolate_path(path, state)
54
56
  end
55
57
 
56
58
  def required_output_specs(step)
57
- outputs = Array(step["outputs"]).select { |item| item.is_a?(Hash) && item["required"] == true }
58
- outputs << step["meta_output"] if step["meta_output"].is_a?(Hash)
59
- outputs
59
+ ArtifactSpecs.for_step(step).select { |item| item["required"] == true }
60
+
60
61
  end
61
62
 
62
63
  def actual_content_issues(path, checks)
@@ -143,12 +144,13 @@ def generate_machine_evidence(root, feature, state_revision, branch, expected_pa
143
144
  packed_paths = Array(pack["files"]).select { |item| item.is_a?(Hash) && item["required"] == true }.map { |item| item["path"].to_s }.sort
144
145
  errors << "machine evidence required outputs mismatch" unless packed_paths == expected_paths.sort
145
146
  errors << "machine evidence contains a missing required file" unless Array(pack["files"]).all? { |item| item.is_a?(Hash) && item["exists"] == true && item["sha256"].to_s.match?(/\A[0-9a-f]{64}\z/) }
146
- raise ArgumentError, errors.join("; ") unless errors.empty?
147
+ recovery_check(errors.empty?, errors.join("; "))
147
148
 
148
149
  output_fingerprint = Digest::SHA256.hexdigest(
149
150
  Array(pack["files"]).map { |item| [item["path"].to_s, item["sha256"].to_s] }.sort.to_json,
150
151
  )
151
152
  {
153
+ "valid" => errors.empty?,
152
154
  "evidence_path" => evidence_path,
153
155
  "evidence_sha256" => Digest::SHA256.file(absolute).hexdigest,
154
156
  "output_fingerprint" => output_fingerprint,
@@ -213,6 +215,13 @@ rescue JSON::ParserError => error
213
215
  { "ok" => false, "reason" => "join_returned_invalid_json", "message" => error.message, "stderr" => stderr.strip, "exit_status" => status.exitstatus }
214
216
  end
215
217
 
218
+ def recovery_check(condition, message)
219
+ return if condition
220
+
221
+ $recovery_advisories << GatePolicy.advisory({ "code" => "parallel_recovery_check_failed", "message" => message })
222
+ end
223
+
224
+ $recovery_advisories = []
216
225
  root = Dir.pwd
217
226
  feature = options[:feature].to_s
218
227
  result = nil
@@ -244,7 +253,7 @@ begin
244
253
  raise ArgumentError, "parallel run not found" unless run
245
254
  recovery = recovery_id(run, options[:expected_state_revision], options[:expected_run_revision])
246
255
  branches = Array(run["branches"])
247
- already_joined = state["parallel_run_ref"].nil? && run["status"] == "completed" && branches.any? && branches.all? { |branch| branch["status"] == "succeeded" }
256
+ already_joined = state["parallel_run_ref"].nil? && run["status"] == "completed" && branches.any? && (branches.all? { |branch| branch["status"] == "succeeded" } || run["completion_with_warnings"])
248
257
  if already_joined
249
258
  append_audit(root, feature, {
250
259
  "schema_version" => 1, "origin" => "core.parallel_recovery/v1", "event" => "idempotent_noop",
@@ -264,7 +273,7 @@ begin
264
273
  raise ArgumentError, "parallel run is bound to stale STATE revision" unless Integer(run.fetch("bound_state_revision")) == state_revision
265
274
  raise ArgumentError, "parallel run has no branches" if branches.empty?
266
275
  ineligible = branches.select { |branch| %w[failed aborted].include?(branch["status"].to_s) }
267
- raise ArgumentError, "failed/aborted branches require explicit retry: #{ineligible.map { |branch| branch['branch_id'] }.join(', ')}" unless ineligible.empty?
276
+ recovery_check(ineligible.empty?, "failed/aborted branches were not retried: #{ineligible.map { |branch| branch['branch_id'] }.join(', ')}")
268
277
  previous_validation = prior_validation(root, feature, recovery, run)
269
278
  unless current_run_revision == options[:expected_run_revision] || previous_validation
270
279
  raise ArgumentError, "stale parallel run revision: expected=#{options[:expected_run_revision]}, actual=#{current_run_revision}"
@@ -275,9 +284,10 @@ begin
275
284
  registry_source = workflow.dig("artifact_registry", "source").to_s
276
285
  raise ArgumentError, "workflow artifact_registry.source is required for recovery" if registry_source.empty?
277
286
  registry_path, = DevflowControlIO.resolve_control_path(root, registry_source, :registry)
278
- registry = load_yaml_mapping(registry_path, "artifact registry")
279
- raise ArgumentError, "artifact registry revision mismatch" unless Integer(registry.fetch("state_revision")) == state_revision
280
- raise ArgumentError, "artifact registry feature mismatch" unless registry.dig("registry_scope", "feature").to_s == feature
287
+ registry = File.file?(registry_path) ? load_yaml_mapping(registry_path, "artifact registry") : {}
288
+ recovery_check(!registry.empty?, "artifact registry missing")
289
+ recovery_check(registry["state_revision"] == state_revision, "artifact registry revision mismatch")
290
+ recovery_check(registry.dig("registry_scope", "feature").to_s == feature, "artifact registry feature mismatch")
281
291
  registry_by_path = Array(registry["artifacts"]).each_with_object({}) do |artifact, index|
282
292
  raise ArgumentError, "artifact registry entry must be a mapping" unless artifact.is_a?(Hash)
283
293
  path = artifact["path"].to_s
@@ -292,7 +302,8 @@ begin
292
302
  raise ArgumentError, "parallel branch workflow step is unknown: #{branch['workflow_step']}" unless step
293
303
  artifacts = validate_registered_outputs(root, workflow, state, step, registry_by_path)
294
304
  invalid = artifacts.reject { |artifact| artifact["valid"] }
295
- raise ArgumentError, "branch #{branch['branch_id']} artifact validation failed: #{invalid.to_json}" unless invalid.empty?
305
+ recovery_check(invalid.empty?, "branch #{branch['branch_id']} artifact validation failed: #{invalid.to_json}")
306
+ next unless invalid.empty?
296
307
  machine = generate_machine_evidence(root, feature, state_revision, branch, artifacts.map { |artifact| artifact["path"] })
297
308
  validation_records << {
298
309
  "branch_id" => branch["branch_id"], "workflow_step" => branch["workflow_step"],
@@ -310,6 +321,7 @@ begin
310
321
 
311
322
  validation_records.each do |validated|
312
323
  machine = validated.fetch("machine_evidence")
324
+ next unless machine["valid"]
313
325
  updated = ParallelRun.update_branch(
314
326
  root: root, feature: feature, branch_id: validated.fetch("branch_id"),
315
327
  expected_revision: current_run_revision, status: "succeeded",
@@ -320,9 +332,7 @@ begin
320
332
  recovered_branches << validated.fetch("branch_id")
321
333
  end
322
334
  final_run = ParallelRun.load(root, feature)
323
- unless Array(final_run["branches"]).all? { |branch| branch["status"] == "succeeded" }
324
- raise ArgumentError, "recovery did not produce an all-succeeded run"
325
- end
335
+ recovery_check(Array(final_run["branches"]).all? { |branch| branch["status"] == "succeeded" }, "recovery did not produce an all-succeeded run; branch statuses preserved")
326
336
  append_audit(root, feature, {
327
337
  "schema_version" => 1, "origin" => "core.parallel_recovery/v1", "event" => "branches_recovered",
328
338
  "recovery_id" => recovery, "reason" => options[:reason], "run_id" => final_run["run_id"],
@@ -382,5 +392,6 @@ rescue StandardError => error
382
392
  result = { "ok" => false, "reason" => "parallel_recovery_refused", "message" => error.message, "recovery_id" => recovery }
383
393
  end
384
394
 
395
+ result["warnings"] = $recovery_advisories + Array(result.dig("details", "advisories")) unless $recovery_advisories.empty?
385
396
  puts(options[:json] ? JSON.generate(result) : JSON.pretty_generate(result))
386
397
  exit exit_status
@@ -6,6 +6,8 @@ require "json"
6
6
  require "optparse"
7
7
  require "yaml"
8
8
  require_relative "control_io"
9
+ require_relative "artifact_specs"
10
+ require_relative "gate_policy"
9
11
  require_relative "validate_state"
10
12
  require_relative "phase_ownership"
11
13
 
@@ -30,7 +32,7 @@ end
30
32
  def build_registry(root, workflow, state)
31
33
  checks = workflow.dig("artifact_registry", "content_checks") || {}
32
34
  artifacts = Array(workflow["steps"]).flat_map do |step|
33
- Array(step["outputs"]).map do |output|
35
+ ArtifactSpecs.for_step(step).map do |output|
34
36
  path = interpolate_path(output.fetch("path"), state)
35
37
  abs, = DevflowControlIO.resolve_control_path(root, path, :artifact)
36
38
  exists = File.exist?(abs)
@@ -90,9 +92,7 @@ begin
90
92
  output_path, = DevflowControlIO.resolve_control_path(root, path, :artifact)
91
93
  path unless File.exist?(output_path)
92
94
  end.compact
93
- if missing_current_outputs.any?
94
- result = { "ok" => false, "classification" => "unsafe", "reason" => "missing_required_output", "missing_outputs" => missing_current_outputs, "plan" => { "state_transition" => "none", "registry_write" => false } }
95
- elsif !validation["ok"]
95
+ if !validation["ok"]
96
96
  result = { "ok" => false, "classification" => "unsafe", "reason" => "ambiguous_or_invalid_state", "issues" => validation["errors"], "plan" => { "state_transition" => "none", "registry_write" => false } }
97
97
  elsif !options[:confirm] || !options[:write]
98
98
  result = { "ok" => false, "classification" => "safe_repairable", "reason" => "confirmation_required", "revision" => state.fetch("revision", 0), "plan" => { "state_transition" => "none", "registry_write" => true, "registry" => candidate } }
@@ -8,6 +8,8 @@ require "json"
8
8
  require "optparse"
9
9
  require "yaml"
10
10
  require_relative "control_io"
11
+ require_relative "artifact_specs"
12
+ require_relative "gate_policy"
11
13
  require_relative "phase_ownership"
12
14
 
13
15
  options = {
@@ -56,7 +58,7 @@ end
56
58
 
57
59
  def collect_outputs(workflow)
58
60
  Array(workflow["steps"]).flat_map do |step|
59
- Array(step["outputs"]).map do |output|
61
+ ArtifactSpecs.for_step(step).map do |output|
60
62
  [step, output]
61
63
  end
62
64
  end
@@ -67,6 +69,10 @@ project_lock = DevflowControlIO.acquire_project_lock(root) if options[:write]
67
69
  begin
68
70
  state_path, = DevflowControlIO.resolve_control_path(root, options[:state], :state)
69
71
  state = load_state(state_path)
72
+ if state["current_flow"] == "dynamic"
73
+ puts JSON.generate({ "ok" => true, "written" => false, "mode" => "dynamic", "reason" => "task evidence is maintained in STATE; no per-skill artifact registry required" })
74
+ exit 0
75
+ end
70
76
  ownership = PhaseOwnership.validate_state(root: root, state: state)
71
77
  unless ownership["ok"]
72
78
  puts JSON.pretty_generate(ownership.merge("written" => false))
@@ -33,8 +33,7 @@ module WorkflowStateValidator
33
33
  # Core quality checks are advisory by default. Keep only structural and
34
34
  # write-safety failures hard; AI review decides whether business-quality
35
35
  # drift, stale metadata, or incomplete generated evidence should pause work.
36
- SOFT_GATE_MODE = ENV.fetch("DEVFLOW_SOFT_GATES", "1") != "0"
37
- HARD_VALIDATION_CODES = %w[
36
+ OPERATION_VALIDATION_CODES = %w[
38
37
  invalid_control_path
39
38
  missing_state_file
40
39
  missing_state_front_matter
@@ -44,36 +43,8 @@ module WorkflowStateValidator
44
43
  workflow_step_unknown
45
44
  phase_ownership_conflict
46
45
  ].freeze
47
- # Acceptance-execution trust failures are never downgraded by soft gates.
48
- ACCEPTANCE_HARD_VALIDATION_CODES = %w[
49
- acceptance_results_not_passed
50
- execution_surface_mismatch
51
- approval_invalid
52
- human_result_unapproved
53
- dsl_not_approved
54
- environment_profile_not_approved
55
- runtime_integrity_error
56
- acceptance_tests_not_passed
57
- acceptance_tests_empty
58
- acceptance_test_suite_freeze_failed
59
- acceptance_test_suite_freeze_attestation_invalid
60
- acceptance_test_suite_freeze_attestation_failed
61
- acceptance_test_suite_candidate_binding_failed
62
- acceptance_test_suite_freeze_recovery_failed
63
- ].freeze
64
-
65
- def self.hard_validation_code?(code)
66
- value = code.to_s
67
- return true if HARD_VALIDATION_CODES.include?(value)
68
- return true if ACCEPTANCE_HARD_VALIDATION_CODES.include?(value)
69
- return true if value.start_with?("artifact_registry_", "parallel_", "PARALLEL_")
70
- return true if value.start_with?("duplicate_", "unknown_", "missing_tracked_step")
71
- return true if value.start_with?("step_in_", "completed_", "terminal_", "non_completed_")
72
- return true if value.start_with?("waiting_checkpoint_")
73
- return true if value.start_with?("skipped_")
74
- return true if %w[missing_required_output output_not_registered].include?(value)
75
-
76
- false
46
+ def self.operation_validation_code?(code)
47
+ OPERATION_VALIDATION_CODES.include?(code.to_s)
77
48
  end
78
49
 
79
50
  module_function
@@ -89,6 +60,15 @@ module WorkflowStateValidator
89
60
  issues = []
90
61
 
91
62
  state = state_override || load_state(state_path, issues)
63
+ if state && state["current_flow"] == "dynamic"
64
+ require_relative "dynamic_plan"
65
+ begin
66
+ DynamicPlan.validate!(root, state.fetch("plan"))
67
+ rescue ArgumentError, KeyError => error
68
+ issues << issue("error", "invalid_dynamic_plan", error.message)
69
+ end
70
+ return result(issues.empty?, state_path, nil, nil, issues)
71
+ end
92
72
  return result(false, state_path, nil, nil, issues) unless state
93
73
 
94
74
  REQUIRED_STATE_FIELDS.each do |field|
@@ -159,14 +139,12 @@ module WorkflowStateValidator
159
139
  end
160
140
  end
161
141
 
162
- if SOFT_GATE_MODE
163
- issues.each do |entry|
164
- next unless entry["severity"] == "error"
165
- next if hard_validation_code?(entry["code"])
142
+ issues.each do |entry|
143
+ next unless entry["severity"] == "error"
144
+ next if operation_validation_code?(entry["code"])
166
145
 
167
- entry["severity"] = "warning"
168
- entry["soft_gate"] = true
169
- end
146
+ entry["severity"] = "warning"
147
+ entry["soft_gate"] = true
170
148
  end
171
149
  ok = issues.none? { |entry| entry["severity"] == "error" }
172
150
  result(ok, state_path, workflow_path, registry_path, issues)
@@ -107,10 +107,46 @@ def canonical_fingerprint(canonical)
107
107
  end
108
108
 
109
109
  def read_source_fingerprint(root, feature)
110
- path = File.join(root, ".devflow/planning", "requirements", feature, "traceability.json")
111
- return "" unless File.file?(path)
110
+ units_path, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/requirements/#{feature}/source_units.yaml", :artifact)
111
+ # Core source unitization owns this value. Both the baseline template and
112
+ # graph-generated traceability views legitimately omit the legacy mirror.
113
+ traceability_path, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/requirements/#{feature}/traceability.json", :artifact)
114
+ legacy = if File.file?(traceability_path)
115
+ traceability = JSON.parse(File.read(traceability_path))
116
+ raise ArgumentError, "traceability.json must be an object" unless traceability.is_a?(Hash)
117
+ traceability["source_document_fingerprint"].to_s
118
+ else
119
+ ""
120
+ end
121
+ unless File.exist?(units_path) || File.symlink?(units_path)
122
+ return legacy unless legacy.strip.empty?
112
123
 
113
- JSON.parse(File.read(path))["source_document_fingerprint"].to_s
124
+ raise ArgumentError, "source_units.yaml is missing and traceability.json has no legacy source_document_fingerprint; restore requirement-wayfinder source provenance"
125
+ end
126
+
127
+ units = YAML.safe_load(File.read(units_path), permitted_classes: [], aliases: false)
128
+ raise ArgumentError, "source_units.yaml must be an object" unless units.is_a?(Hash)
129
+ fingerprint = units["source_document_fingerprint"].to_s
130
+ digest = fingerprint.delete_prefix("sha256:")
131
+ unless digest.match?(/\A[0-9a-f]{64}\z/)
132
+ raise ArgumentError, "source_units.yaml source_document_fingerprint must be a SHA-256 digest"
133
+ end
134
+ source = units["source_document"].to_s
135
+ if source.empty? || Pathname.new(source).absolute? || source.split(/[\\\/]/).include?("..")
136
+ raise ArgumentError, "source_units.yaml source_document must be a project-relative path"
137
+ end
138
+ root_real = File.realpath(root)
139
+ source_path = File.realpath(File.join(root_real, source))
140
+ unless source_path.start_with?(root_real + File::SEPARATOR)
141
+ raise ArgumentError, "source_units.yaml source_document escapes project root"
142
+ end
143
+ unless Digest::SHA256.file(source_path).hexdigest == digest
144
+ raise ArgumentError, "source_units.yaml source_document_fingerprint is stale; regenerate source units and review the changed requirements"
145
+ end
146
+ unless legacy.empty? || legacy.delete_prefix("sha256:") == digest
147
+ raise ArgumentError, "traceability.json source_document_fingerprint conflicts with source_units.yaml; reconcile requirement provenance"
148
+ end
149
+ fingerprint
114
150
  end
115
151
 
116
152
  def verify_business_action_refs(canonical, path)
@@ -13,13 +13,12 @@ phase_scope:
13
13
  failure_branch_enabled: true
14
14
  parallel_window_enabled: true
15
15
 
16
- # P2 preset: new work fail-closes at the defined checkpoints. Existing STATE
16
+ # Gate checks are advisory. Existing STATE
17
17
  # files without governance_started_revision remain legacy_unattested and are
18
18
  # only reported until their next newly-started governed step.
19
19
  governance:
20
20
  # Chain skill owns orchestration while Core remains an evidence/diagnostic
21
- # service. Set to `core` only after the hard-gate migration is complete.
22
- hard_gate_mode: chain
21
+ # service. Core checks only produce diagnostics.
23
22
  gate_contract_version: 3
24
23
  requirement_graph_gate: enforced
25
24
  requirement_coverage_gate: enforced
@@ -9,7 +9,6 @@ entry:
9
9
  initial_step: regression-test-design
10
10
 
11
11
  governance:
12
- hard_gate_mode: core
13
12
  gate_contract_version: 3
14
13
  requirement_graph_gate: enforced
15
14
  enforced_checkpoints:
@@ -3,11 +3,19 @@ name: workflow-pipeline
3
3
  description: Use when running or resuming the full .devflow/planning delivery pipeline from requirements through implementation, frozen acceptance tests, and Devflow release closeout.
4
4
  ---
5
5
 
6
+ Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
7
+
8
+
9
+ ## 默认软门禁策略
10
+
11
+ 工作流硬门禁已移除。下文及各步骤中的产物、索引、合同、审查、质量、恢复和 CP 审批条件均为诊断建议:保留失败证据和缺项,允许继续执行。用户说“下一步”即可推进,无需额外批准弹窗;不得把继续执行记录成人工批准,也不得把失败、未执行的测试改写为通过。真实读写/执行失败、无法解析的状态和并发版本冲突须如实报告。硬门禁及其恢复开关已移除。
12
+
13
+
6
14
  <objective>
7
15
  全链路研发交付入口。解析用户参数,先确保需求主链“入口分诊 → 探索地图 → 领域建模 → 可实现性审查”完成,再检测当前状态,然后委托 workflow-orchestrator 接管后续所有步骤调度。
8
16
  pipeline 本身不包含执行规则——所有门禁、CP 协议、失败分支、并行窗口、lessons 注入规则均由 orchestrator 维护。
9
17
 
10
- 当前迁移阶段由本 skill 作为链路编排器:按 `next_step.skill` 依次调用各技能、传递上一技能的产物与诊断、再决定下一技能。Core 的 contract/checker/output/review 硬门禁暂不参与阻断;Core 结果只作为事实和风险提示。只有控制面损坏、危险写入或需要人工确认时才停止链路。
18
+ 当前迁移阶段由本 skill 作为链路编排器:按 `next_step.skill` 依次调用各技能、传递上一技能的产物与诊断、再决定下一技能。Core 的 contract/checker/output/review 硬门禁暂不参与阻断;Core 结果只作为事实和风险提示。真实操作无法完成时报告原因;CP 默认带警告继续,仅显式恢复确认时等待人工决定。
11
19
  </objective>
12
20
 
13
21
  <process>
@@ -55,10 +63,10 @@ pipeline 本身不包含执行规则——所有门禁、CP 协议、失败分
55
63
 
56
64
  - **`--track A`**:后端/API 轨道产物模式。可跑 `requirement-triage → requirement-wayfinder → domain-modeling → requirement-feasibility-review → requirement-baseline → api-contract → architecture-feasibility → backend-tech-spec → api-call-guide`,但不能自动进入 `integration-verify / change-plan / execute`。
57
65
  - **`--track B`**:前端/UI 轨道产物模式。要求 `api-contract` 与 `architecture-feasibility` 已完成;新需求从需求主链开始,已有共同事实可跑 `ui-spec → feature-breakdown → frontend-tech-spec`,但不能自动进入 `change-plan / execute`。
58
- - **未设置 track**:完整交付模式。必须完成双轨并通过 `integration-verify` 后才能进入 `change-plan`。
66
+ - **未设置 track**:完整交付模式。完成双轨并执行 `integration-verify` 检查;未通过项作为风险传给 `change-plan`。
59
67
  - **`--no-review`**:保留参数位,但当前双轨 workflow 不自动跳过 CP-2。Merge Gate 不能跳过。
60
68
 
61
- 双轨制原则:API 契约是分叉前的共同事实,`integration-verify` 是合并前的强门禁。单轨只产出局部轨道文档,不代表完整交付。
69
+ 双轨制原则:API 契约是分叉前的共同事实,`integration-verify` 是合并前的软门禁。单轨只产出局部轨道文档,不代表完整交付。
62
70
 
63
71
  `workflow-generate-mainflow-cases` 不是独立 workflow step,而是 `run-acceptance-tests` 绑定阶段的组成部分:对执行面为 `ui` / `api_flow` 且绑定为 `automated` 的 TC,必须先生成 v1 DSL 并经 `devflow_approval` 批准执行计划,否则该 TC 保持 blocked。人工 TC 结果同样只经 `human_result` 批准计入聚合门禁。
64
72
 
@@ -72,7 +80,7 @@ pipeline 本身不包含执行规则——所有门禁、CP 协议、失败分
72
80
  2. `workflow-wayfinder` 建立目标、已知事实、边界、迷雾与 ticket;只有活动切片的 L1/L2 迷雾收束后才可完成。
73
81
  3. `workflow-domain-modeling` 固化共同语言、所有权、状态和边缘案例。
74
82
  4. `requirement-feasibility-review` 以流程证明、代码事实和外部能力检查作为进入基线前的最终门禁。
75
- 5. 仅当审查 meta 的 `baseline_gate = pass` 时,才可进入 `workflow-requirement-baseline`。
83
+ 5. 检查审查 meta 的 `baseline_gate`,未通过项保留为诊断并传给 `workflow-requirement-baseline`。
76
84
 
77
85
  若分诊结论为 `already_done`、`wontfix`、`ready_for_human` 或 `needs_info`,停止主链并报告最小下一步。`workflow-demand-clarify` 不再作为固定主链步骤;需要业务问答时,由 Wayfinder 的 Grilling ticket 调用。
78
86
 
@@ -113,7 +121,7 @@ devflow_state(action="inspect")
113
121
 
114
122
  ## 3. 委托 orchestrator
115
123
 
116
- 在当前 `hard_gate_mode: chain` 下,pipeline 继续调用 `workflow-orchestrator` 获取步骤拓扑,但由本链路规则串联实际技能调用,不把 Core gate 的 `block` 结果当作自动停止条件:
124
+ 当前pipeline 继续调用 `workflow-orchestrator` 获取步骤拓扑,但由本链路规则串联实际技能调用,不把 Core gate 的 `block` 结果当作自动停止条件:
117
125
 
118
126
  ```
119
127
  /workflow-orchestrator
@@ -41,7 +41,7 @@ workflow-pipeline
41
41
  → 完成
42
42
  ```
43
43
 
44
- `workflow-generate-mainflow-cases` 不参与独立排序,而是 `run-acceptance-tests` 绑定阶段的组成部分:automated 的 ui / api_flow TC 需要先生成 v1 DSL 并批准执行计划。人工验收结果(经 `human_result` 批准)会与自动执行结果聚合,验收失败是硬门禁。
44
+ `workflow-generate-mainflow-cases` 不参与独立排序,而是 `run-acceptance-tests` 绑定阶段的组成部分:automated 的 ui / api_flow TC 需要先生成 v1 DSL 并批准执行计划。人工验收结果(经 `human_result` 批准)会与自动执行结果聚合,验收失败是软门禁:保留 failed/blocked 结果与风险,默认不阻止流程推进或收口。
45
45
 
46
46
  ## 双轨与 Checkpoint 规则
47
47
 
@@ -3,6 +3,8 @@ name: workflow-project-context
3
3
  description: "Use when a project needs database-backed initialization, repository discovery, project facts, code indexing, navigation, or initialization recovery."
4
4
  ---
5
5
 
6
+ 门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
+
6
8
  # Project Initialization
7
9
 
8
10
  Initialize project context only. Do not create requirements, approved API contracts, prototypes, roadmaps, commits, tags, repositories, or workflow state.
@@ -3,6 +3,8 @@ name: workflow-api-call-guide
3
3
  description: "Use after backend-tech-spec, before integration-verify. Generates a step-by-step API call guide for frontend developers. This is NOT a change list — no code changes, just calling instructions based on the confirmed API contract and backend tech spec."
4
4
  ---
5
5
 
6
+ 门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
+
6
8
  # API 调用指南
7
9
 
8
10
  ## 定位
@@ -3,6 +3,8 @@ name: workflow-api-contract
3
3
  description: "Use after requirement-baseline, before dual-track fork. Defines shared API contracts (endpoints, request/response shapes, data models) that both Track A and Track B reference. This is the fork point — contract changes after this step are exception paths."
4
4
  ---
5
5
 
6
+ 门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
+
6
8
  # API 契约生成器
7
9
 
8
10
  ## 定位
@@ -27,7 +29,7 @@ description: "Use after requirement-baseline, before dual-track fork. Defines sh
27
29
  - `.devflow/planning/features/{功能名}/api-contract.md`
28
30
  - `.devflow/planning/features/{功能名}/api-contract.meta.json`
29
31
 
30
- The meta file must also set `traceability_status: "passed"` only after every API design has been written to `traceability.json.designs` and `traceability_check.py --stage design` succeeds; otherwise set `failed` and do not advance.
32
+ The meta file must also set `traceability_status: "passed"` only after every API design has been written to `traceability.json.designs` and `traceability_check.py --stage design` succeeds; otherwise set `failed`, record the traceability gaps, and continue with these advisories in soft mode.
31
33
 
32
34
  ## 输入
33
35
 
@@ -3,6 +3,8 @@ name: workflow-api-supplement
3
3
  description: "Use when Track B (frontend-tech-spec) discovers API gaps that the shared API contract doesn't cover. This is an EXCEPTION PATH — not a normal pipeline step. Submits API supplement requests for Track A evaluation."
4
4
  ---
5
5
 
6
+ 门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
+
6
8
  # API 补充需求(异常路径)
7
9
 
8
10
  ## 定位
@@ -3,6 +3,8 @@ name: workflow-architecture-feasibility
3
3
  description: "Use after API contract and before frontend/backend technical specifications to compare implementation paths against confirmed workload facts and repository-static context. Use for async or scheduled work, batch operations, external calls, fanout, migrations, workload/SLA constraints, or when a feature may not fit the current framework and integrations."
4
4
  ---
5
5
 
6
+ 门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
7
+
6
8
  # 架构可行性与方案比较
7
9
 
8
10
  生成一份由 Core 派生的架构简报;不把自由 Markdown 当作架构真相,也不把仓库声明当成生产容量。