@guanlin4924/devflow-pi 0.1.39 → 0.1.47
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 +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +728 -39
- package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +13 -0
- package/plugins/devflow-core/skills/wireframe-proposal/agents/openai.yaml +4 -0
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-review/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/agents/openai.yaml +4 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +16 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-candidate-delta.schema.yaml +41 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-claim.schema.yaml +10 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model-policy.yaml +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model.schema.yaml +241 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-source-registry.schema.yaml +80 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/proposal.schema.yaml +44 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/questions.schema.yaml +14 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +13 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +334 -30
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +77 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_closure_e2e_seed.json +516 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +76 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_impact.py +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +108 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +570 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_gate.rb +89 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_render.py +131 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1063 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +365 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +94 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +84 -6
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +45 -12
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +248 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +139 -14
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph_backfill.rb +143 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +15 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +45 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +86 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +222 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +231 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +457 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +94 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +111 -1
- package/plugins/devflow-implementation/agents/implementation-worker.md +6 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +107 -37
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +20 -1
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +1 -1
- package/src/constants.ts +1 -1
- package/src/runtime.ts +61 -24
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "json"
|
|
4
5
|
require_relative "gate_result"
|
|
5
6
|
require_relative "gate_contract"
|
|
6
7
|
require_relative "checker_registry"
|
|
8
|
+
require_relative "parallel_run"
|
|
9
|
+
require_relative "business_model_gate"
|
|
7
10
|
|
|
8
11
|
module GateRuntime
|
|
9
|
-
WORKFLOW_STATUSES = %w[in_progress waiting_checkpoint completed blocked failed cancelled remediation].freeze
|
|
12
|
+
WORKFLOW_STATUSES = %w[not_started in_progress waiting_checkpoint completed blocked failed cancelled remediation].freeze
|
|
10
13
|
|
|
11
14
|
module_function
|
|
12
15
|
|
|
16
|
+
def evaluate_business_model(violations:, transition_kind:, gate_id: "business-model-closure", step_id: "business-model")
|
|
17
|
+
BusinessModelGate.evaluate(violations: violations, transition_kind: transition_kind, gate_id: gate_id, step_id: step_id)
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
def validate_contract!(contract)
|
|
14
21
|
GateContract.validate!(contract)
|
|
15
22
|
CheckerRegistry.validate!(Array(contract.dig("quality", "checker_ids")))
|
|
16
23
|
contract
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
def evaluate(contract:, workflow_state:, current_revision:, expected_revision:, track: nil, source: "harness", evidence: {}, root: nil, check_outputs: false, transition_kind: nil, checkpoint_authorized: false)
|
|
26
|
+
def evaluate(contract:, workflow_state:, current_revision:, expected_revision:, track: nil, branch_id: nil, attempt: nil, branch_revision: nil, source: "harness", evidence: {}, root: nil, check_outputs: false, transition_kind: nil, checkpoint_authorized: false)
|
|
20
27
|
validate_contract!(contract)
|
|
21
28
|
gate_id = contract.fetch("gate_id")
|
|
22
29
|
step_id = contract.fetch("step_id")
|
|
@@ -25,18 +32,34 @@ module GateRuntime
|
|
|
25
32
|
unless WORKFLOW_STATUSES.include?(workflow_state["status"].to_s)
|
|
26
33
|
return GateResult.block(**base, code: "WORKFLOW_STATUS_INVALID", message: "workflow status is not recognized")
|
|
27
34
|
end
|
|
28
|
-
|
|
35
|
+
if workflow_state["parallel_run_ref"]
|
|
36
|
+
branch_error = validate_parallel_branch(
|
|
37
|
+
contract: contract,
|
|
38
|
+
workflow_state: workflow_state,
|
|
39
|
+
current_revision: current_revision,
|
|
40
|
+
track: track,
|
|
41
|
+
branch_id: branch_id,
|
|
42
|
+
attempt: attempt,
|
|
43
|
+
branch_revision: branch_revision,
|
|
44
|
+
evidence: evidence,
|
|
45
|
+
root: root,
|
|
46
|
+
transition_kind: transition_kind,
|
|
47
|
+
)
|
|
48
|
+
return GateResult.block(**base, **branch_error) if branch_error
|
|
49
|
+
end
|
|
50
|
+
unless %w[confirm_checkpoint enter_failure_branch exit_failure_branch parallel_join].include?(transition_kind)
|
|
29
51
|
allowed_tracks = Array(contract.dig("topology", "allowed_tracks"))
|
|
30
52
|
if track && !allowed_tracks.empty? && !allowed_tracks.include?(track)
|
|
31
53
|
return GateResult.block(**base, code: "TRACK_SCOPE_VIOLATION", message: "current track is not allowed by GateContract", recovery: { "kind" => "registered_reentry" })
|
|
32
54
|
end
|
|
33
55
|
allowed_predecessors = Array(contract.dig("topology", "allowed_predecessors"))
|
|
56
|
+
allowed_intervening = Array(contract.dig("topology", "allowed_intervening_predecessors"))
|
|
34
57
|
previous = workflow_state["last_completed_step"] || workflow_state["previous_step"]
|
|
35
|
-
if previous && !allowed_predecessors.empty? && !allowed_predecessors.include?(previous)
|
|
58
|
+
if previous && !allowed_predecessors.empty? && !allowed_predecessors.include?(previous) && !allowed_intervening.include?(previous)
|
|
36
59
|
return GateResult.block(**base, code: "STEP_PREDECESSOR_INVALID", message: "previous step is not allowed by GateContract")
|
|
37
60
|
end
|
|
38
61
|
if root
|
|
39
|
-
missing = required_paths(contract.dig("entry", "required_inputs"), root, workflow_state)
|
|
62
|
+
missing = required_paths(contract.dig("entry", "required_inputs"), root, workflow_state, allow_missing_collections: true)
|
|
40
63
|
return GateResult.block(**base, code: "REQUIRED_INPUT_MISSING", message: "required gate input is missing", details: { "missing" => missing }) unless missing.empty?
|
|
41
64
|
if check_outputs
|
|
42
65
|
missing_outputs = required_paths(contract.dig("outputs", "required"), root, workflow_state)
|
|
@@ -60,6 +83,58 @@ module GateRuntime
|
|
|
60
83
|
GateResult.block(gate_id: contract.is_a?(Hash) ? contract["gate_id"] : "unknown", step_id: contract.is_a?(Hash) ? contract["step_id"] : "unknown", code: "GATE_CONTRACT_INVALID", message: e.message)
|
|
61
84
|
end
|
|
62
85
|
|
|
86
|
+
def validate_parallel_branch(contract:, workflow_state:, current_revision:, track:, branch_id:, attempt:, branch_revision:, evidence:, root:, transition_kind:)
|
|
87
|
+
return { code: "PARALLEL_CONTEXT_MISSING", message: "parallel workflow requires root" } unless root
|
|
88
|
+
|
|
89
|
+
record = ParallelRun.load(root, workflow_state.fetch("current_feature"))
|
|
90
|
+
return { code: "PARALLEL_RUN_NOT_FOUND", message: "parallel_run_ref does not resolve to a run record" } unless record
|
|
91
|
+
return { code: "PARALLEL_RUN_REF_MISMATCH", message: "STATE parallel_run_ref does not match run record" } unless record["run_id"].to_s == workflow_state["parallel_run_ref"].to_s
|
|
92
|
+
bound_revision = Integer(record.fetch("bound_state_revision"))
|
|
93
|
+
return { code: "PARALLEL_STATE_REVISION_MISMATCH", message: "parallel run is bound to a different STATE revision", details: { expected_state_revision: bound_revision, actual_state_revision: current_revision } } unless bound_revision == current_revision.to_i
|
|
94
|
+
|
|
95
|
+
if transition_kind.to_s == "parallel_join"
|
|
96
|
+
branches = Array(record["branches"])
|
|
97
|
+
return { code: "PARALLEL_JOIN_INCOMPLETE", message: "parallel join requires every branch to succeed", details: { incomplete: branches.reject { |item| item["status"].to_s == "succeeded" }.map { |item| item["branch_id"] } } } unless branches.any? && branches.all? { |item| item["status"].to_s == "succeeded" }
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
return { code: "PARALLEL_CONTEXT_MISSING", message: "parallel workflow requires branch_id" } unless branch_id
|
|
101
|
+
|
|
102
|
+
branch = Array(record["branches"]).find { |item| item["branch_id"].to_s == branch_id.to_s }
|
|
103
|
+
return { code: "PARALLEL_BRANCH_NOT_FOUND", message: "branch is not registered in the parallel run", details: { branch_id: branch_id } } unless branch
|
|
104
|
+
return { code: "PARALLEL_STEP_MISMATCH", message: "branch is bound to a different workflow step", details: { branch_step: branch["workflow_step"], gate_step: contract["step_id"] } } unless branch["workflow_step"].to_s == contract["step_id"].to_s
|
|
105
|
+
expected_track = branch["track"].to_s.sub(/\Atrack_/, "").upcase
|
|
106
|
+
return { code: "PARALLEL_TRACK_MISMATCH", message: "branch track is not the requested track", details: { branch_track: expected_track, requested_track: track } } unless track.to_s.empty? || expected_track.empty? || expected_track == track.to_s
|
|
107
|
+
return { code: "PARALLEL_BRANCH_TERMINAL", message: "terminal branch cannot pass a gate; retry must create a new attempt" } if ParallelRun::TERMINAL_BRANCH_STATUSES.include?(branch["status"].to_s) && transition_kind.to_s != "parallel_branch_retry"
|
|
108
|
+
return { code: "PARALLEL_ATTEMPT_MISMATCH", message: "branch attempt does not match the current run record" } if attempt && Integer(branch.fetch("attempt", 0)) != attempt.to_i
|
|
109
|
+
return { code: "PARALLEL_BRANCH_REVISION_MISMATCH", message: "branch revision does not match the current run record" } if branch_revision && Integer(branch.fetch("branch_revision", 0)) != branch_revision.to_i
|
|
110
|
+
|
|
111
|
+
predecessor_ids = Array(contract.dig("topology", "allowed_predecessors"))
|
|
112
|
+
completed = Array(workflow_state["completed_steps"]).map(&:to_s)
|
|
113
|
+
completed.concat(Array(record["branches"]).select { |item| item["status"].to_s == "succeeded" }.map { |item| item["workflow_step"].to_s })
|
|
114
|
+
missing_predecessors = predecessor_ids.map(&:to_s).reject { |step| completed.include?(step) }
|
|
115
|
+
return { code: "PARALLEL_PREDECESSOR_INCOMPLETE", message: "parallel branch predecessor is incomplete", details: { missing: missing_predecessors } } unless missing_predecessors.empty?
|
|
116
|
+
|
|
117
|
+
if transition_kind.to_s == "parallel_branch_complete"
|
|
118
|
+
required = %w[branch_id attempt branch_revision]
|
|
119
|
+
missing = required.reject { |field| evidence.key?(field) }
|
|
120
|
+
return { code: "PARALLEL_EVIDENCE_MISSING", message: "branch completion requires independent evidence binding", details: { missing: missing } } unless missing.empty?
|
|
121
|
+
return { code: "PARALLEL_EVIDENCE_MISMATCH", message: "branch evidence does not match current branch", details: { branch_id: branch_id } } unless evidence["branch_id"].to_s == branch_id.to_s && evidence["attempt"].to_i == branch.fetch("attempt", 0).to_i && evidence["branch_revision"].to_i == branch.fetch("branch_revision", 0).to_i
|
|
122
|
+
evidence_path = evidence["evidence_path"].to_s
|
|
123
|
+
return { code: "PARALLEL_EVIDENCE_MISSING", message: "Core machine evidence file is required" } if evidence_path.empty? || !File.file?(File.expand_path(evidence_path, root))
|
|
124
|
+
begin
|
|
125
|
+
pack = JSON.parse(File.read(File.expand_path(evidence_path, root)))
|
|
126
|
+
binding = pack.dig("evidence", "parallel_binding")
|
|
127
|
+
expected = { "run_id" => record["run_id"], "branch_id" => branch["branch_id"], "attempt" => branch.fetch("attempt", 0), "branch_revision" => branch.fetch("branch_revision", 0), "state_revision" => current_revision.to_i }
|
|
128
|
+
return { code: "PARALLEL_EVIDENCE_MISMATCH", message: "machine evidence binding does not match current branch", details: { expected: expected, actual: binding } } unless binding.is_a?(Hash) && expected.all? { |key, value| binding[key].to_s == value.to_s }
|
|
129
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
130
|
+
return { code: "PARALLEL_EVIDENCE_MISMATCH", message: "machine evidence file is not valid JSON" }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
nil
|
|
134
|
+
rescue KeyError, ArgumentError, TypeError
|
|
135
|
+
{ code: "PARALLEL_RECORD_INVALID", message: "parallel run or branch record is invalid" }
|
|
136
|
+
end
|
|
137
|
+
|
|
63
138
|
def run_checker(contract:, workflow_state:, current_revision:, expected_revision:, checker:, **kwargs)
|
|
64
139
|
boundary = evaluate(contract: contract, workflow_state: workflow_state, current_revision: current_revision, expected_revision: expected_revision, **kwargs)
|
|
65
140
|
return boundary unless boundary["verdict"] == "allow"
|
|
@@ -73,13 +148,16 @@ module GateRuntime
|
|
|
73
148
|
)
|
|
74
149
|
end
|
|
75
150
|
|
|
76
|
-
def required_paths(entries, root, state)
|
|
151
|
+
def required_paths(entries, root, state, allow_missing_collections: false)
|
|
77
152
|
Array(entries).each_with_object([]) do |entry, missing|
|
|
78
153
|
raw = entry.is_a?(Hash) ? entry["path"] : entry
|
|
79
154
|
next if raw.to_s.empty?
|
|
80
155
|
relative = raw.to_s.gsub("{feature}", state.fetch("current_feature", "").to_s).gsub("{phase}", state.fetch("current_phase", "").to_s)
|
|
81
156
|
absolute = File.expand_path(relative, root)
|
|
82
157
|
root_real = File.realpath(root)
|
|
158
|
+
root_expanded = File.expand_path(root)
|
|
159
|
+
inside_root = absolute.start_with?("#{root_expanded}#{File::SEPARATOR}")
|
|
160
|
+
next if allow_missing_collections && raw.to_s.end_with?(File::SEPARATOR) && inside_root && !File.exist?(absolute)
|
|
83
161
|
actual = File.exist?(absolute) ? File.realpath(absolute) : absolute
|
|
84
162
|
next if actual.start_with?("#{root_real}#{File::SEPARATOR}") && File.exist?(actual)
|
|
85
163
|
missing << relative
|
|
@@ -191,6 +191,9 @@ def active_parallel_window(workflow, state, root, step_by_id, completed_ids, pen
|
|
|
191
191
|
step = step_by_id[step_id]
|
|
192
192
|
pending_ids.include?(step_id) && step && dependencies_satisfied?(step, completed_ids)
|
|
193
193
|
end
|
|
194
|
+
# A single runnable branch is an intentional serial fallback (for example
|
|
195
|
+
# when active_track limits the window to one track), not a missing window.
|
|
196
|
+
# Only a genuinely multi-branch runnable set enters the parallel scheduler.
|
|
194
197
|
next unless runnable_ids.length > 1
|
|
195
198
|
|
|
196
199
|
policy = window["execution_policy"] || {}
|
|
@@ -12,13 +12,17 @@ require_relative "control_io"
|
|
|
12
12
|
require_relative "requirement_graph"
|
|
13
13
|
require_relative "checker_registry"
|
|
14
14
|
|
|
15
|
-
options = { feature: nil, step: nil, contract: nil, write: false, json: false }
|
|
15
|
+
options = { feature: nil, step: nil, contract: nil, write: false, json: false, branch_id: nil, attempt: nil, branch_revision: nil, lock_held: false }
|
|
16
16
|
OptionParser.new do |o|
|
|
17
17
|
o.on("--feature NAME") { |v| options[:feature] = v }
|
|
18
18
|
o.on("--step ID") { |v| options[:step] = v }
|
|
19
19
|
o.on("--contract PATH") { |v| options[:contract] = v }
|
|
20
20
|
o.on("--write") { options[:write] = true }
|
|
21
21
|
o.on("--json") { options[:json] = true }
|
|
22
|
+
o.on("--parallel-branch-id ID") { |v| options[:branch_id] = v }
|
|
23
|
+
o.on("--parallel-attempt N", Integer) { |v| options[:attempt] = v }
|
|
24
|
+
o.on("--parallel-branch-revision N", Integer) { |v| options[:branch_revision] = v }
|
|
25
|
+
o.on("--lock-held") { options[:lock_held] = true }
|
|
22
26
|
end.parse!
|
|
23
27
|
abort "feature, step and --write are required" unless options[:feature] && options[:step] && options[:write]
|
|
24
28
|
abort "invalid feature" unless RequirementGraph.valid_feature?(options[:feature])
|
|
@@ -41,7 +45,7 @@ def file_evidence(root, relative)
|
|
|
41
45
|
{ "path" => relative, "exists" => true, "sha256" => Digest::SHA256.file(absolute).hexdigest, "bytes" => File.size(absolute) }
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
def independent_evidence(root, feature, step, state, workflow, graph, files, checker_results)
|
|
48
|
+
def independent_evidence(root, feature, step, state, workflow, graph, files, checker_results, binding = nil, inputs = [])
|
|
45
49
|
source_nodes = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "source" }
|
|
46
50
|
authority = source_nodes.map do |node|
|
|
47
51
|
evidence = node["evidence"].is_a?(Hash) ? node["evidence"] : {}
|
|
@@ -54,7 +58,9 @@ def independent_evidence(root, feature, step, state, workflow, graph, files, che
|
|
|
54
58
|
# The harness may publish an append-only event log beside the machine pack.
|
|
55
59
|
# Absence is represented as unavailable, never as false/zero, so the pack
|
|
56
60
|
# cannot manufacture proof that an engine did not invoke AI.
|
|
57
|
-
|
|
61
|
+
binding_attempt = binding && binding["attempt"]
|
|
62
|
+
event_name = binding_attempt.nil? ? "#{step}.engine-events.jsonl" : "#{step}.attempt-#{binding_attempt}.engine-events.jsonl"
|
|
63
|
+
event_path = File.join(".planning", "evidence", feature, event_name)
|
|
58
64
|
event_absolute, = DevflowControlIO.resolve_control_path(root, event_path, :artifact)
|
|
59
65
|
events = if File.file?(event_absolute)
|
|
60
66
|
File.foreach(event_absolute).first(10_000).each_with_object([]) do |line, parsed|
|
|
@@ -72,10 +78,15 @@ def independent_evidence(root, feature, step, state, workflow, graph, files, che
|
|
|
72
78
|
nil
|
|
73
79
|
end
|
|
74
80
|
ai_events = events ? events.select { |event| %w[ai_runtime_initialized ai_invocation].include?(event["event"]) } : []
|
|
81
|
+
dispatched_events = events ? events.select { |event| event["event"] == "engine_dispatched" } : []
|
|
82
|
+
ai_measurement_observed = !ai_events.empty?
|
|
75
83
|
|
|
76
84
|
{
|
|
77
85
|
"collector" => "core.machine_evidence/v1",
|
|
78
86
|
"state" => { "revision" => state["revision"], "step" => step },
|
|
87
|
+
"parallel_binding" => binding,
|
|
88
|
+
"input_fingerprints" => inputs.select { |item| item["exists"] }.map { |item| item["sha256"] },
|
|
89
|
+
"output_fingerprints" => files.select { |item| item["exists"] }.map { |item| item["sha256"] },
|
|
79
90
|
"source_authority" => { "status" => authority.empty? ? "unavailable" : "observed", "sources" => authority },
|
|
80
91
|
"candidate_binding" => {
|
|
81
92
|
"status" => "observed",
|
|
@@ -90,8 +101,13 @@ def independent_evidence(root, feature, step, state, workflow, graph, files, che
|
|
|
90
101
|
"engine_evidence" => {
|
|
91
102
|
"collector" => events ? "harness.engine-events/v1" : "unavailable",
|
|
92
103
|
"event_log" => event_path,
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
# engine_dispatched proves that the harness dispatched work, but it does
|
|
105
|
+
# not prove how many model calls the child session made. Keep unknown as
|
|
106
|
+
# nil instead of manufacturing false/zero values for branch evidence.
|
|
107
|
+
"ai_runtime_initialized" => ai_measurement_observed ? ai_events.any? { |event| event["event"] == "ai_runtime_initialized" } : nil,
|
|
108
|
+
"ai_invocation_count" => ai_measurement_observed ? ai_events.count { |event| event["event"] == "ai_invocation" } : nil,
|
|
109
|
+
"engine_dispatched_count" => events ? dispatched_events.length : nil,
|
|
110
|
+
"ai_capable_engine_dispatched" => events ? dispatched_events.any? { |event| event["ai_capable"] == true } : nil,
|
|
95
111
|
"evidence_status" => events ? "observed" : "unavailable"
|
|
96
112
|
}
|
|
97
113
|
}
|
|
@@ -156,10 +172,21 @@ rescue JSON::ParserError => error
|
|
|
156
172
|
end
|
|
157
173
|
|
|
158
174
|
root = Dir.pwd
|
|
159
|
-
lock = DevflowControlIO.acquire_project_lock(root)
|
|
175
|
+
lock = options[:lock_held] ? nil : DevflowControlIO.acquire_project_lock(root)
|
|
160
176
|
begin
|
|
161
177
|
state = state_front_matter(root)
|
|
162
|
-
|
|
178
|
+
binding = nil
|
|
179
|
+
branch = nil
|
|
180
|
+
if options[:branch_id]
|
|
181
|
+
require_relative "parallel_run"
|
|
182
|
+
record = ParallelRun.load(root, options[:feature])
|
|
183
|
+
branch = Array(record && record["branches"]).find { |item| item["branch_id"].to_s == options[:branch_id].to_s }
|
|
184
|
+
raise "parallel branch not found" unless record && branch
|
|
185
|
+
raise "parallel branch attempt mismatch" unless options[:attempt].nil? || Integer(branch.fetch("attempt", 0)) == options[:attempt]
|
|
186
|
+
raise "parallel branch revision mismatch" unless options[:branch_revision].nil? || Integer(branch.fetch("branch_revision", 0)) == options[:branch_revision]
|
|
187
|
+
binding = { "run_id" => record["run_id"], "branch_id" => branch["branch_id"], "attempt" => branch["attempt"], "branch_revision" => branch["branch_revision"], "state_revision" => state["revision"] }
|
|
188
|
+
end
|
|
189
|
+
unless state["current_feature"] == options[:feature] && (state["current_step"] == options[:step] || branch)
|
|
163
190
|
result = { "ok" => false, "reason" => "state_step_mismatch", "state_feature" => state["current_feature"], "state_step" => state["current_step"] }
|
|
164
191
|
puts JSON.pretty_generate(result)
|
|
165
192
|
exit 1
|
|
@@ -181,6 +208,7 @@ begin
|
|
|
181
208
|
{ "path" => relative, "required" => true, "exists" => exists, "bytes" => exists ? File.size(absolute) : 0,
|
|
182
209
|
"sha256" => exists ? Digest::SHA256.file(absolute).hexdigest : nil }
|
|
183
210
|
end.compact
|
|
211
|
+
input_files = Array(step["inputs"]).map { |input| file_evidence(root, interpolate(input.to_s, state)) }
|
|
184
212
|
|
|
185
213
|
graph, = RequirementGraph.load_graph(root, options[:feature])
|
|
186
214
|
checkers = RequirementGraph.checker_results(graph)
|
|
@@ -206,13 +234,17 @@ begin
|
|
|
206
234
|
contract_hash = Digest::SHA256.file(contract_path).hexdigest
|
|
207
235
|
end
|
|
208
236
|
node_counts = Array(graph["nodes"]).group_by { |node| node["kind"] }.transform_values(&:length)
|
|
209
|
-
independent = independent_evidence(root, options[:feature], options[:step], state, workflow, graph, files, checker_results)
|
|
237
|
+
independent = independent_evidence(root, options[:feature], options[:step], state, workflow, graph, files, checker_results, binding, input_files)
|
|
210
238
|
engine_evidence_complete = independent.dig("engine_evidence", "evidence_status") == "observed"
|
|
211
239
|
evidence_complete = engine_evidence_gate != "enforced" || engine_evidence_complete
|
|
212
240
|
pack = {
|
|
213
241
|
"schema_version" => 2, "origin" => "core_machine", "feature" => options[:feature], "step" => options[:step],
|
|
214
242
|
"state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw),
|
|
215
|
-
"
|
|
243
|
+
"run_id" => binding && binding["run_id"], "branch_id" => binding && binding["branch_id"],
|
|
244
|
+
"attempt" => binding && binding["attempt"], "branch_revision" => binding && binding["branch_revision"],
|
|
245
|
+
"input_fingerprints" => input_files.select { |item| item["exists"] }.map { |item| item["sha256"] },
|
|
246
|
+
"output_fingerprints" => files.select { |item| item["exists"] }.map { |item| item["sha256"] },
|
|
247
|
+
"contract_hash" => contract_hash, "files" => files, "input_files" => input_files, "checkers" => checker_results, "evidence" => independent,
|
|
216
248
|
"counters" => { "required_file_count" => files.length, "existing_file_count" => files.count { |item| item["exists"] }, "nodes_by_kind" => node_counts,
|
|
217
249
|
"stale_node_count" => Array(graph["stale"]).length, "stale_view_count" => Hash(graph["views"]).count { |_name, view| view["needs_recheck"] } },
|
|
218
250
|
"view_fingerprints" => Hash(graph["views"]).transform_values { |view| view.select { |key, _value| %w[fingerprint graph_revision state needs_recheck stale_fingerprint].include?(key) } },
|
|
@@ -220,12 +252,13 @@ begin
|
|
|
220
252
|
"evidence_complete" => evidence_complete,
|
|
221
253
|
"gate_mode" => gate.empty? ? "observe" : gate
|
|
222
254
|
}.compact
|
|
223
|
-
|
|
255
|
+
suffix = binding ? ".attempt-#{binding["attempt"]}.branch-#{Digest::SHA256.hexdigest(binding["branch_id"].to_s)[0, 12]}" : ""
|
|
256
|
+
destination = File.join(root, ".planning/evidence", options[:feature], "#{options[:step]}#{suffix}.machine.json")
|
|
224
257
|
DevflowControlIO.atomic_write(destination, JSON.pretty_generate(pack) + "\n")
|
|
225
|
-
result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "complete" => pack["complete"], "state_revision" => pack["state_revision"], "graph_revision" => pack["graph_revision"], "checkers" => checker_results }
|
|
258
|
+
result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "complete" => pack["complete"], "state_revision" => pack["state_revision"], "graph_revision" => pack["graph_revision"], "checkers" => checker_results, "parallel_binding" => binding }
|
|
226
259
|
rescue StandardError => error
|
|
227
260
|
result = { "ok" => false, "reason" => "machine_evidence_failed", "message" => error.message }
|
|
228
261
|
ensure
|
|
229
|
-
DevflowControlIO.release_lock(lock)
|
|
262
|
+
DevflowControlIO.release_lock(lock) if lock
|
|
230
263
|
end
|
|
231
264
|
puts JSON.pretty_generate(result)
|