@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.
- package/README.md +20 -1
- package/package.json +3 -3
- package/plugins/devflow-core/extensions/devflow-core.ts +339 -20
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +394 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +86 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +192 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +829 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +11 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +39 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +309 -23
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +311 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +53 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +254 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +109 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/uat-remediation.yaml +21 -1
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +38 -0
- package/plugins/devflow-implementation/agents/implementation-worker.md +3 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +7 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +2 -0
- package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -3
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +63 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/agents/openai.yaml +4 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +7 -6
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +3 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +49 -3
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +48 -0
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/agents/openai.yaml +4 -0
- 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
|
-
|
|
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
|
|