@guanlin4924/devflow-pi 0.1.19 → 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 +19 -0
- package/package.json +3 -3
- package/plugins/devflow-core/extensions/devflow-core.ts +142 -2
- 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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Core-owned freeze point for a feature's derived TC suite. A skill may propose
|
|
5
|
+
# individual candidates, but it cannot turn them into evidence by confirming
|
|
6
|
+
# them one by one: this command snapshots the complete, fresh suite under CP-3.
|
|
7
|
+
require "json"
|
|
8
|
+
require "open3"
|
|
9
|
+
require "optparse"
|
|
10
|
+
require_relative "control_io"
|
|
11
|
+
require_relative "requirement_graph"
|
|
12
|
+
|
|
13
|
+
options = { feature: nil, expected_graph_revision: nil, cp_record: nil, write: false, json: false }
|
|
14
|
+
command = ARGV.shift
|
|
15
|
+
OptionParser.new do |parser|
|
|
16
|
+
parser.on("--feature NAME") { |value| options[:feature] = value }
|
|
17
|
+
parser.on("--expected-graph-revision REVISION", Integer) { |value| options[:expected_graph_revision] = value }
|
|
18
|
+
parser.on("--cp-record RECORD") { |value| options[:cp_record] = value }
|
|
19
|
+
parser.on("--write") { options[:write] = true }
|
|
20
|
+
parser.on("--json") { options[:json] = true }
|
|
21
|
+
end.parse!
|
|
22
|
+
|
|
23
|
+
def test_nodes(graph)
|
|
24
|
+
Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def api_test_matrix_check(root, feature, graph)
|
|
28
|
+
return { "ok" => true, "skipped" => "no_api_nodes" } unless Array(graph["nodes"]).any? { |node| node.is_a?(Hash) && node["kind"] == "api" }
|
|
29
|
+
|
|
30
|
+
script = File.join(__dir__, "api_test_matrix.py")
|
|
31
|
+
stdout, stderr, status = Open3.capture3("python3", script, "validate", "--feature", feature, "--json", chdir: root)
|
|
32
|
+
payload = JSON.parse(stdout)
|
|
33
|
+
return payload if status.success? && payload.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
{ "ok" => false, "errors" => [stderr.strip.empty? ? "API test matrix checker returned invalid output" : stderr.strip] }
|
|
36
|
+
rescue JSON::ParserError
|
|
37
|
+
{ "ok" => false, "errors" => ["API test matrix checker returned invalid JSON"] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
root = Dir.pwd
|
|
41
|
+
lock = nil
|
|
42
|
+
begin
|
|
43
|
+
raise ArgumentError, "command must be inspect or freeze" unless %w[inspect freeze].include?(command)
|
|
44
|
+
raise ArgumentError, "--feature is required" unless RequirementGraph.valid_feature?(options[:feature])
|
|
45
|
+
lock = DevflowControlIO.acquire_project_lock(root) if command == "freeze"
|
|
46
|
+
graph, path, relative = RequirementGraph.load_graph(root, options[:feature])
|
|
47
|
+
cases = test_nodes(graph)
|
|
48
|
+
|
|
49
|
+
if command == "inspect"
|
|
50
|
+
view = RequirementGraph.view_payload(graph, "tests")
|
|
51
|
+
result = {
|
|
52
|
+
"ok" => true,
|
|
53
|
+
"path" => relative,
|
|
54
|
+
"graph_revision" => graph["revision"],
|
|
55
|
+
"test_case_ids" => cases.map { |node| node["id"] },
|
|
56
|
+
"frozen" => view["frozen"],
|
|
57
|
+
"frozen_graph_revision" => view["frozen_graph_revision"],
|
|
58
|
+
"stale" => Array(graph["stale"]).select { |entry| cases.any? { |node| node["id"] == entry["node_id"] } }
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "--write is required" unless options[:write]
|
|
62
|
+
raise ArgumentError, "--expected-graph-revision is required" if options[:expected_graph_revision].nil?
|
|
63
|
+
raise ArgumentError, "--cp-record is required" if options[:cp_record].to_s.strip.empty?
|
|
64
|
+
if graph["revision"] != options[:expected_graph_revision]
|
|
65
|
+
result = { "ok" => false, "reason" => "stale_graph_revision", "expected_revision" => options[:expected_graph_revision], "actual_revision" => graph["revision"] }
|
|
66
|
+
elsif cases.empty?
|
|
67
|
+
result = { "ok" => false, "reason" => "test_suite_empty", "message" => "cannot freeze an empty TC suite" }
|
|
68
|
+
elsif cases.any? { |node| node["status"] == "confirmed" }
|
|
69
|
+
result = { "ok" => false, "reason" => "test_suite_already_frozen", "message" => "confirmed test cases require a new candidate suite" }
|
|
70
|
+
elsif Array(graph["stale"]).any?
|
|
71
|
+
result = { "ok" => false, "reason" => "test_suite_graph_stale", "message" => "cannot freeze TC suite while the requirement graph has stale downstream nodes" }
|
|
72
|
+
else
|
|
73
|
+
matrix = api_test_matrix_check(root, options[:feature], graph)
|
|
74
|
+
result = if matrix["ok"]
|
|
75
|
+
RequirementGraph.freeze_test_cases!(root, graph, options[:cp_record])
|
|
76
|
+
else
|
|
77
|
+
{ "ok" => false, "reason" => "api_test_matrix_incomplete", "message" => Array(matrix["errors"]).join("; "), "matrix" => matrix }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
rescue ArgumentError, JSON::ParserError => error
|
|
82
|
+
result = { "ok" => false, "reason" => "acceptance_test_suite_invalid", "message" => error.message }
|
|
83
|
+
ensure
|
|
84
|
+
DevflowControlIO.release_lock(lock)
|
|
85
|
+
end
|
|
86
|
+
puts JSON.pretty_generate(result)
|
|
@@ -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
|
|