@guanlin4924/devflow-pi 0.1.18 → 0.1.20

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 (30) hide show
  1. package/README.md +20 -1
  2. package/package.json +3 -3
  3. package/plugins/devflow-core/extensions/devflow-core.ts +339 -20
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +394 -0
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +86 -0
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +192 -1
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +829 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -5
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +11 -1
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +5 -1
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +39 -3
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +309 -23
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +311 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +53 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +254 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +109 -4
  17. package/plugins/devflow-core/skills/workflow-orchestrator/templates/uat-remediation.yaml +21 -1
  18. package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +38 -0
  19. package/plugins/devflow-implementation/agents/implementation-worker.md +3 -0
  20. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +7 -0
  21. package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +2 -0
  22. package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -3
  23. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +63 -0
  24. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/agents/openai.yaml +4 -0
  25. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +7 -6
  26. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +3 -0
  27. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +49 -3
  28. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +48 -0
  29. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/agents/openai.yaml +4 -0
  30. package/plugins/devflow-quality/skills/workflow-verify-work/SKILL.md +9 -3
@@ -2,13 +2,16 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "date"
5
+ require "digest"
5
6
  require "fileutils"
6
7
  require "json"
8
+ require "open3"
7
9
  require "optparse"
8
10
  require "yaml"
9
11
  require_relative "control_io"
10
12
  require_relative "validate_state"
11
13
  require_relative "phase_ownership"
14
+ require_relative "requirement_graph"
12
15
 
13
16
  options = {
14
17
  state: nil,
@@ -321,7 +324,11 @@ end
321
324
 
322
325
  def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
323
326
  track = active_track(state)
324
- return pending_ids.first unless track
327
+ unless track
328
+ return pending_ids.find do |step_id|
329
+ step_by_id[step_id] && dependencies_satisfied?(step_by_id[step_id], completed_ids)
330
+ end
331
+ end
325
332
 
326
333
  allowed = track_allowed_steps(track)
327
334
  pending_ids.find do |step_id|
@@ -342,6 +349,97 @@ def fail_result(reason, details = {})
342
349
  { "ok" => false, "reason" => reason, "details" => details }
343
350
  end
344
351
 
352
+ # CP-3 has to validate a *candidate* task/TC binding before it freezes the
353
+ # derived TC suite. The normal checker then validates the Core attestation
354
+ # against the frozen suite before execution. Keeping both calls here, under
355
+ # the state lock, prevents the old circular gate ("must be frozen before the
356
+ # checkpoint that freezes it") without adding another user confirmation.
357
+ def test_task_binding_check(root, feature, stage)
358
+ script = File.join(__dir__, "test_task_binding_check.py")
359
+ stdout, stderr, status = Open3.capture3("python3", script, "--feature", feature, "--stage", stage, "--json", chdir: root)
360
+ payload = JSON.parse(stdout)
361
+ passed = status.success? && payload.is_a?(Hash) && payload["ok"] == true
362
+ return payload.merge("ok" => true) if passed
363
+
364
+ { "ok" => false, "reason" => "test_task_binding_#{stage}_check_failed",
365
+ "errors" => payload.is_a?(Hash) && payload["errors"].is_a?(Array) ? payload["errors"] : [stderr.strip.empty? ? "checker returned invalid JSON" : stderr.strip] }
366
+ rescue JSON::ParserError => error
367
+ { "ok" => false, "reason" => "test_task_binding_#{stage}_check_failed", "errors" => ["checker returned invalid JSON: #{error.message}"] }
368
+ end
369
+
370
+ def api_test_matrix_check(root, feature, graph)
371
+ return { "ok" => true, "skipped" => "no_api_nodes" } unless Array(graph["nodes"]).any? { |node| node.is_a?(Hash) && node["kind"] == "api" }
372
+
373
+ script = File.join(__dir__, "api_test_matrix.py")
374
+ stdout, stderr, status = Open3.capture3("python3", script, "validate", "--feature", feature, "--json", chdir: root)
375
+ payload = JSON.parse(stdout)
376
+ return payload if status.success? && payload.is_a?(Hash)
377
+
378
+ { "ok" => false, "errors" => [stderr.strip.empty? ? "API test matrix checker returned invalid output" : stderr.strip] }
379
+ rescue JSON::ParserError => error
380
+ { "ok" => false, "errors" => ["API test matrix checker returned invalid JSON: #{error.message}"] }
381
+ end
382
+
383
+ def api_test_schema_check(root, feature, graph)
384
+ return { "ok" => true, "skipped" => "no_api_nodes" } unless Array(graph["nodes"]).any? { |node| node.is_a?(Hash) && node["kind"] == "api" }
385
+
386
+ script = File.join(__dir__, "api_test_matrix.py")
387
+ stdout, stderr, status = Open3.capture3("python3", script, "check", "--feature", feature, "--json", chdir: root)
388
+ payload = JSON.parse(stdout)
389
+ return payload if status.success? && payload.is_a?(Hash)
390
+
391
+ { "ok" => false, "errors" => [stderr.strip.empty? ? "API test schema checker returned invalid output" : stderr.strip] }
392
+ rescue JSON::ParserError => error
393
+ { "ok" => false, "errors" => ["API test schema checker returned invalid JSON: #{error.message}"] }
394
+ end
395
+
396
+ def test_suite_freeze_attestation_path(root, feature)
397
+ DevflowControlIO.resolve_control_path(root, ".planning/features/#{feature}/test-suite-freeze.json", :artifact)
398
+ end
399
+
400
+ def frozen_test_suite_pin(freeze_result)
401
+ view = freeze_result["tests_view"]
402
+ return nil unless view.is_a?(Hash)
403
+
404
+ { "graph_revision" => view["graph_revision"], "fingerprint" => view["fingerprint"] }
405
+ end
406
+
407
+ def write_test_suite_freeze_attestation(root, feature, state_revision, candidate_check, freeze_result)
408
+ candidate_suite = candidate_check["candidate_test_suite"]
409
+ frozen_suite = frozen_test_suite_pin(freeze_result)
410
+ raise DevflowControlIO::InvalidControlPath, "candidate test suite pin missing from CP-3 binding check" unless candidate_suite.is_a?(Hash)
411
+ raise DevflowControlIO::InvalidControlPath, "frozen test suite pin missing from CP-3 freeze" unless frozen_suite.is_a?(Hash)
412
+ binding_hash = candidate_check["binding_sha256"]
413
+ raise DevflowControlIO::InvalidControlPath, "test binding hash missing from CP-3 binding check" unless binding_hash.is_a?(String) && !binding_hash.empty?
414
+
415
+ path, relative = test_suite_freeze_attestation_path(root, feature)
416
+ payload = {
417
+ "schema_version" => 1,
418
+ "origin" => "core_acceptance_test_suite_freeze",
419
+ "feature" => feature,
420
+ "checkpoint" => "CP-3",
421
+ "state_revision" => state_revision,
422
+ "candidate_test_suite" => candidate_suite,
423
+ "frozen_test_suite" => frozen_suite,
424
+ "test_binding_sha256" => binding_hash,
425
+ "frozen_case_ids" => Array(freeze_result["frozen_case_ids"]).sort
426
+ }
427
+ DevflowControlIO.atomic_create(path, JSON.pretty_generate(payload) + "\n")
428
+ DevflowControlIO.register_control_artifact(root, {
429
+ "path" => relative, "type" => "test-suite-freeze-attestation", "feature" => feature,
430
+ "origin" => "core_acceptance_test_suite_freeze", "sha256" => Digest::SHA256.file(path).hexdigest
431
+ })
432
+ payload
433
+ end
434
+
435
+ def cp3_freeze_provenance?(graph, state_revision)
436
+ expected = "CP-3: state revision #{state_revision} approved"
437
+ cases = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" }
438
+ !cases.empty? && cases.all? do |node|
439
+ node["status"] == "confirmed" && node.dig("confirmation", "origin") == "core_acceptance_test_suite" && node.dig("confirmation", "cp_record") == expected
440
+ end
441
+ end
442
+
345
443
  # Return from a completed failure branch using its declared return_to contract.
346
444
  # The caller must still explicitly confirm the returned checkpoint before any
347
445
  # subsequent source-flow step can proceed.
@@ -481,6 +579,98 @@ result =
481
579
  elsif Array(state["pending_steps"]).empty?
482
580
  result = fail_result("no_pending_step")
483
581
  else
582
+ # CP-API turns the shared candidate contract into confirmed graph
583
+ # fact inside the same user checkpoint. Skills cannot self-confirm
584
+ # API nodes and downstream generated TC imports can require a
585
+ # reviewed contract revision.
586
+ if state["checkpoint"] == "CP-API" && steps.any? { |step| step["id"] == "api-contract" }
587
+ feature = state.fetch("current_feature")
588
+ graph, = RequirementGraph.load_graph(root, feature)
589
+ schema_check = api_test_schema_check(root, feature, graph)
590
+ if !schema_check["ok"]
591
+ result = fail_result("api_test_schema_incomplete", "api_test_schema" => schema_check)
592
+ else
593
+ api_confirmation = RequirementGraph.confirm_api_contracts!(root, graph, "CP-API: state revision #{current_revision} approved")
594
+ if api_confirmation["ok"]
595
+ actions << "api_contract_confirmed" unless Array(api_confirmation["confirmed_api_ids"]).empty?
596
+ else
597
+ result = fail_result("api_contract_confirmation_failed", "api_contract" => api_confirmation)
598
+ end
599
+ end
600
+ end
601
+
602
+ # CP-3 authorizes exactly one immutable TC freeze. It is deliberately
603
+ # inside the checkpoint transition so the user does not have to
604
+ # approve an extra model-issued dialog and a Skill cannot freeze its
605
+ # own candidates early.
606
+ if result.nil? && state["checkpoint"] == "CP-3" && steps.any? { |step| step["id"] == "acceptance-test-design" }
607
+ feature = state.fetch("current_feature")
608
+ graph, = RequirementGraph.load_graph(root, feature)
609
+ attestation_path, = test_suite_freeze_attestation_path(root, feature)
610
+
611
+ if File.exist?(attestation_path)
612
+ frozen_check = test_task_binding_check(root, feature, "frozen")
613
+ unless frozen_check["ok"]
614
+ result = fail_result("acceptance_test_suite_freeze_attestation_invalid", "test_suite" => frozen_check)
615
+ else
616
+ actions << "acceptance_test_suite_freeze_attestation_verified"
617
+ end
618
+ elsif cp3_freeze_provenance?(graph, current_revision)
619
+ # A process can die after graph/view writes but before the
620
+ # attestation is published. Recover only if each TC proves that
621
+ # this exact CP-3 already froze it; never use this path to bless
622
+ # a generic confirmed suite.
623
+ recovery_check = test_task_binding_check(root, feature, "recovery")
624
+ unless recovery_check["ok"]
625
+ result = fail_result("acceptance_test_suite_freeze_recovery_failed", "test_suite" => recovery_check)
626
+ else
627
+ recovered = {
628
+ "ok" => true,
629
+ "tests_view" => graph.dig("views", "tests"),
630
+ "frozen_case_ids" => Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" }.map { |node| node["id"] }
631
+ }
632
+ begin
633
+ write_test_suite_freeze_attestation(root, feature, current_revision, recovery_check, recovered)
634
+ frozen_check = test_task_binding_check(root, feature, "frozen")
635
+ if frozen_check["ok"]
636
+ actions << "acceptance_test_suite_freeze_attestation_recovered"
637
+ else
638
+ result = fail_result("acceptance_test_suite_freeze_attestation_invalid", "test_suite" => frozen_check)
639
+ end
640
+ rescue DevflowControlIO::InvalidControlPath, SystemCallError => error
641
+ result = fail_result("acceptance_test_suite_freeze_attestation_failed", "message" => error.message)
642
+ end
643
+ end
644
+ else
645
+ matrix_check = api_test_matrix_check(root, feature, graph)
646
+ unless matrix_check["ok"]
647
+ result = fail_result("api_test_matrix_incomplete", "api_test_matrix" => matrix_check)
648
+ else
649
+ candidate_check = test_task_binding_check(root, feature, "candidate")
650
+ unless candidate_check["ok"]
651
+ result = fail_result("acceptance_test_suite_candidate_binding_failed", "test_suite" => candidate_check)
652
+ else
653
+ frozen = RequirementGraph.freeze_test_cases!(root, graph, "CP-3: state revision #{current_revision} approved")
654
+ unless frozen["ok"]
655
+ result = fail_result("acceptance_test_suite_freeze_failed", "test_suite" => frozen)
656
+ else
657
+ begin
658
+ write_test_suite_freeze_attestation(root, feature, current_revision, candidate_check, frozen)
659
+ frozen_check = test_task_binding_check(root, feature, "frozen")
660
+ if frozen_check["ok"]
661
+ actions << "acceptance_test_suite_frozen"
662
+ else
663
+ result = fail_result("acceptance_test_suite_freeze_attestation_invalid", "test_suite" => frozen_check)
664
+ end
665
+ rescue DevflowControlIO::InvalidControlPath, SystemCallError => error
666
+ result = fail_result("acceptance_test_suite_freeze_attestation_failed", "message" => error.message)
667
+ end
668
+ end
669
+ end
670
+ end
671
+ end
672
+ end
673
+ if result.nil?
484
674
  pending = Array(state["pending_steps"])
485
675
  completed = Array(state["completed_steps"])
486
676
  next_id = next_pending_step_id(state, pending, step_by_id, completed)
@@ -505,6 +695,7 @@ result =
505
695
  action_description = "Checkpoint confirmed. Advanced to next step."
506
696
  next_intent_requested = true
507
697
  end
698
+ end
508
699
  end
509
700
  end
510
701