Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions bin/agent-workflow-seam-doctor
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ module AgentWorkflowSeamDoctor
)
[^>\n]*>
}ix
FULL_STRING_ANGLE_PLACEHOLDER = /\A<[^<>\n]*[^\s<>\n][^<>\n]*>\z/
CI_PARITY_KEYWORDS = /(?:CI\ parity\ (?:environment|command)|parity\ environment|runner\ image|reproduction\ guide)/
CI_PARITY_SEAM_PLACEHOLDER = /<(?=[^>\n]*#{CI_PARITY_KEYWORDS.source}\b(?!\s*(?:[[:alnum:]_-]+\s*)*:\s*[^>\s]))[^>\n]*>/i
CI_PARITY_EXECUTABLE_PLACEHOLDER = /<[^>\n]*#{CI_PARITY_KEYWORDS.source}\b[^>\n]*>/i
Comment thread
justin808 marked this conversation as resolved.
Expand Down Expand Up @@ -1659,20 +1660,26 @@ module AgentWorkflowSeamDoctor
REQUIRED_POLICY_KEYS.each do |key|
if !config.key?(key)
issues << "missing policy key: #{key}"
elsif unresolved_policy_value?(config[key])
elsif unresolved_policy_value?(config[key], placeholder_mode: :legacy)
issues << "unresolved policy value for key: #{key}"
end
end

config.each do |key, value|
next if REQUIRED_POLICY_KEYS.include?(key)
next unless unresolved_policy_value?(value)

autonomous_merge = key == "autonomous_merge"
next unless unresolved_policy_value?(
value,
empty_collections_unresolved: !autonomous_merge,
placeholder_mode: autonomous_merge ? :full_string : :legacy
)

issues << "unresolved policy value for key: #{key}"
end

if config.key?(REPO_PREFIX_POLICY_KEY) &&
!unresolved_policy_value?(config[REPO_PREFIX_POLICY_KEY]) &&
!unresolved_policy_value?(config[REPO_PREFIX_POLICY_KEY], placeholder_mode: :legacy) &&
(!config[REPO_PREFIX_POLICY_KEY].is_a?(String) ||
!config[REPO_PREFIX_POLICY_KEY].match?(REPO_PREFIX_PATTERN))
issues << "invalid policy value for key: #{REPO_PREFIX_POLICY_KEY} " \
Expand Down Expand Up @@ -1705,7 +1712,8 @@ module AgentWorkflowSeamDoctor
if !intake_policy.key?(key)
"missing policy key: #{qualified_key}"
elsif !intake_policy[key].is_a?(String) || intake_policy[key].strip.empty? ||
unresolved_policy_value?(intake_policy[key]) || intake_policy[key].strip == "n/a"
unresolved_policy_value?(intake_policy[key], placeholder_mode: :legacy) ||
intake_policy[key].strip == "n/a"
"invalid policy value for key: #{qualified_key}"
end
end
Expand All @@ -1719,14 +1727,19 @@ module AgentWorkflowSeamDoctor
YAML.safe_load(File.read(path, encoding: "UTF-8"), aliases: false) || {}
end

def unresolved_policy_value?(value)
def unresolved_policy_value?(value, placeholder_mode:, empty_collections_unresolved: true)
case value
when String
unresolved_template_value?(value)
unresolved_template_value?(value, placeholder_mode:)
when Array
value.empty? || value.any? { |entry| unresolved_policy_value?(entry) }
(empty_collections_unresolved && value.empty?) || value.any? do |entry|
unresolved_policy_value?(entry, empty_collections_unresolved:, placeholder_mode:)
end
when Hash
value.empty? || value.any? { |key, entry| unresolved_policy_value?(key.to_s) || unresolved_policy_value?(entry) }
(empty_collections_unresolved && value.empty?) || value.any? do |key, entry|
unresolved_policy_value?(key.to_s, empty_collections_unresolved:, placeholder_mode:) ||
unresolved_policy_value?(entry, empty_collections_unresolved:, placeholder_mode:)
end
else
value.nil?
end
Expand Down Expand Up @@ -1758,9 +1771,12 @@ module AgentWorkflowSeamDoctor
nil
end

def unresolved_template_value?(value)
def unresolved_template_value?(value, placeholder_mode:)
stripped = value.strip
stripped.empty? || stripped.match?(SEAM_PLACEHOLDER) || stripped.match?(CI_PARITY_SEAM_PLACEHOLDER)
return true if stripped.empty?
return stripped.match?(FULL_STRING_ANGLE_PLACEHOLDER) if placeholder_mode == :full_string

stripped.match?(SEAM_PLACEHOLDER) || stripped.match?(CI_PARITY_SEAM_PLACEHOLDER)
end

def shared_markdown_paths(root, shared_roots: [])
Expand Down
197 changes: 197 additions & 0 deletions bin/agent-workflow-seam-doctor-test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,48 @@ def test_unresolved_policy_value_fails
end
end

def test_required_policy_values_reject_embedded_legacy_placeholders
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(
root,
POLICY.merge(
"base_branch" => "Use <base branch>",
"ci_parity_environment" => "Run in <runner image>"
)
)
write_skill(root, "No commands here.\n")

out, status = run_doctor(root)

refute status.success?
assert_includes out, "unresolved policy value for key: base_branch"
assert_includes out, "unresolved policy value for key: ci_parity_environment"
end
end

def test_optional_policy_values_recursively_reject_embedded_legacy_placeholders
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(
root,
POLICY.merge(
"custom_scalar" => "Use <base branch>",
"custom_array" => ["Run in <runner image>"],
"custom_mapping" => { "nested" => "Use <main branch>" }
)
)
write_skill(root, "No commands here.\n")

out, status = run_doctor(root)

refute status.success?
%w[custom_scalar custom_array custom_mapping].each do |key|
assert_includes out, "unresolved policy value for key: #{key}"
end
end
end

def test_optional_repo_prefix_accepts_valid_value_and_remains_optional
with_repo do |root|
write_valid_binstub_contract(root)
Expand Down Expand Up @@ -413,6 +455,161 @@ def test_optional_autonomous_merge_policy_uses_the_shared_closed_schema
end
end

def test_optional_autonomous_merge_policy_accepts_an_exact_empty_mapping_seed
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(root, POLICY.merge("autonomous_merge" => {}))
write_skill(root, "No commands here.\n")

policy_text = File.read(File.join(root, ".agents/agent-workflow.yml"), encoding: "UTF-8")
out, status = run_doctor(root)

assert_includes policy_text, "autonomous_merge: {}\n"
assert status.success?, out
end
end

def test_optional_autonomous_merge_policy_accepts_runtime_valid_empty_arrays
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(
root,
POLICY.merge(
"autonomous_merge" => {
"human_review_paths" => [],
"policy_paths" => [],
"generated_paths" => []
}
)
)
write_skill(root, "No commands here.\n")

out, status = run_doctor(root)

assert status.success?, out
end
end

def test_optional_autonomous_merge_policy_rejects_nested_unresolved_placeholders
variants = {
"legacy direct list scalar" => {
"policy_paths" => ["<base branch>"]
},
"legacy nested mapping list scalar" => {
"safe_path_groups" => {
"documentation" => {
"include" => ["docs/**"],
"exclude" => ["<base branch>"]
}
}
},
"threshold relaxation rationale" => {
"thresholds" => { "max_changed_files" => 30 },
"threshold_relaxation" => {
"rationale" => "<nonempty rationale covering all relaxed thresholds>"
}
},
"policy path" => {
"policy_paths" => ["<repo-owned glob>"]
},
"generated path" => {
"generated_paths" => ["<repo-owned generated glob>"]
},
"human-review pattern" => {
"human_review_paths" => [
{
"id" => "repo-owned-risk",
"pattern" => "<repo-owned glob>",
"reason" => "hot-path"
}
]
},
"human-review other detail" => {
"human_review_paths" => [
{
"id" => "repo-owned-risk",
"pattern" => "app/**",
"reason" => "other",
"detail" => "<nonempty repo-owned reason>"
}
]
},
"safe-path include" => {
"safe_path_groups" => {
"documentation" => {
"include" => ["<repo-owned glob>"],
"exclude" => []
}
}
},
"safe-path exclude" => {
"safe_path_groups" => {
"documentation" => {
"include" => ["docs/**"],
"exclude" => ["<repo-owned glob>"]
}
}
}
}
results = variants.transform_values do |autonomous_merge|
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(root, POLICY.merge("autonomous_merge" => autonomous_merge))
write_skill(root, "No commands here.\n")

run_doctor(root)
end
end
failures = results.filter_map do |label, (out, status)|
next unless status.success? || !out.include?("unresolved policy value for key: autonomous_merge")

"#{label}: status=#{status.exitstatus}, output=#{out.inspect}"
end

assert_empty failures, failures.join("\n")
end

def test_optional_autonomous_merge_policy_allows_angle_bracket_text_inside_ordinary_prose
rationales = [
"Document <nonempty rationale covering all relaxed thresholds> after calibration.",
"Document <base branch> as historical context after calibration."
]
rationales.each do |rationale|
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(
root,
POLICY.merge(
"autonomous_merge" => {
"thresholds" => { "max_changed_files" => 30 },
"threshold_relaxation" => { "rationale" => rationale }
}
)
)
write_skill(root, "No commands here.\n")

out, status = run_doctor(root)

assert status.success?, out
end
end
end

def test_empty_collections_outside_autonomous_merge_remain_unresolved
[[], {}].each do |empty_collection|
with_repo do |root|
write_valid_binstub_contract(root)
write_policy(root, POLICY.merge("custom_runtime_paths" => empty_collection))
write_skill(root, "No commands here.\n")

out, status = run_doctor(root)

refute status.success?, empty_collection.class.name
assert_includes out, "unresolved policy value for key: custom_runtime_paths", empty_collection.class.name
end
end
end

def test_invalid_autonomous_merge_policy_is_reported_by_the_seam_doctor
with_repo do |root|
write_valid_binstub_contract(root)
Expand Down
5 changes: 4 additions & 1 deletion skills/pr-batch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ Ask only for missing data. If the user already supplied an exact value, use it.
10. **Lane split**: exact per-machine list, odd/even, labels, area, owner, or another explicit partition.
11. **Permissions**: confirm the current session can run without blocking worker approval prompts.
12. **Question handling**: labels or comments to use for blocking questions, plus where non-blocking decisions should be recorded.
13. **Completion states**: `merged`, `ready-gates-clean`, `ready-no-merge-authority`, `waiting-on-checks-or-review`, `external-gate-failing`, `blocked-user-input`, or `no-pr-evidence`.
13. **Completion states**: `merged`, `ready-gates-clean`, `ready-no-merge-authority`,
`ready-human-review-required`, `autonomous-merge-evidence-unknown`,
`waiting-on-checks-or-review`, `external-gate-failing`, `blocked-user-input`,
or `no-pr-evidence`.

## Canonical Readiness Vocabulary

Expand Down
Loading
Loading