Skip to content

Commit 2026e6c

Browse files
committed
Tighten agent workflow command wrappers
1 parent 29af7a6 commit 2026e6c

7 files changed

Lines changed: 240 additions & 15 deletions

File tree

.agents/agent-workflow.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ ci_parity_environment: "No dedicated act/local runner image; use bin/ci-local an
66
secret_redaction_patterns: "Redact env/log fields whose names contain SECRET, TOKEN, KEY, PASSWORD, CREDENTIAL, CERT, PASSPHRASE, PEM, PRIVATE, DSN, or LICENSE, plus REACT_ON_RAILS_PRO_LICENSE, REACT_ON_RAILS_PRO_LICENSE_V2, BENCHER_API_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN, GH_TOKEN, NPM_OTP, RUBYGEMS_OTP, DOCS_DISPATCH_APP_KEY, RENDERER_PASSWORD, and SECRET_KEY_BASE. Favor conservative over-redaction."
77
trusted_github_actor_boundary: ".agents/trusted-github-actors.yml does not trust github-actions[bot] by default; adding it requires auditing comment-producing workflows and updating preflight policy in the same PR."
88
benchmark_labels: "benchmark, benchmark-core, benchmark-pro, benchmark-pro-node-renderer, hosted-ci-no-benchmarks (suppress); opt-in on PRs."
9-
follow_up_prefix: "Follow-up: (default to no new issue; see the Maintainer Attention Contract)"
9+
# Policy: default to no new issue; see the Maintainer Attention Contract section in AGENTS.md.
10+
follow_up_prefix: "Follow-up:"
1011
changelog: "/CHANGELOG.md, user-visible changes only; [Pro] scope tag; version-stamp via the rake update_changelog task; taxonomy in the Changelog section."
1112
merge_ledger: "script/pr-merge-ledger <PR> --strict — per-PR merge-readiness check emitting changelog classification and a complete_allowed verdict."
1213
review_gate: "claude-review is the preferred independent review check; see the Review Workflow section."

.agents/bin/README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ run `.agents/bin/<name>` in any repo without knowing this repo's specific
55
commands. Each script is a thin, repo-owned wrapper. A script that is **absent**
66
means that capability is n/a here.
77

8-
| Script | Purpose | This repo runs |
9-
| --- | --- | --- |
10-
| `setup` | Install dependencies | `script/bootstrap` |
11-
| `validate` | Pre-push gate (`--changed`/`--all`/`--fast`) | `bin/ci-local` |
12-
| `test` | Run tests | `(cd react_on_rails && bundle exec rake run_rspec)` + `pnpm run test` |
13-
| `lint` | Lint / format (`rake autofix` to fix) | `(cd react_on_rails && bundle exec rake lint)` |
14-
| `build` | Build / type-check | `pnpm run build` + `pnpm run type-check` + `rake rbs:validate` |
15-
| `docs` | Docs checks | `script/check-docs-sidebar` + `bin/check-links` |
16-
| `ci-detect` | CI change detector | `script/ci-changes-detector origin/main` |
8+
| Script | Purpose | This repo runs |
9+
| ----------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
10+
| `setup` | Install dependencies | `bin/setup` |
11+
| `validate` | Pre-push gate (`--changed`/`--all`/`--fast`) | `bin/ci-local` |
12+
| `test` | Run tests | `(cd react_on_rails && bundle exec rake run_rspec:all_but_examples)` + `pnpm run test` |
13+
| `lint` | Lint / format (`rake autofix` to fix) | `(cd react_on_rails && bundle exec rake lint)` + `pnpm run lint` + `pnpm start format.listDifferent` |
14+
| `build` | Build / type-check | `pnpm run build` + `pnpm run type-check` + `rake rbs:validate` |
15+
| `docs` | Docs checks | `script/check-docs-sidebar` + `bin/check-links` |
16+
| `ci-detect` | CI change detector | `script/ci-changes-detector [base-ref]` (default `origin/main`) |
1717

1818
Non-command policy lives in [`../agent-workflow.yml`](../agent-workflow.yml).
19+
Workflow-specific checks such as `actionlint` and `yamllint .github/` stay in the
20+
PR-processing workflow for `.github/**` changes rather than the general build entrypoint.

.agents/bin/agent-workflow-seam-doctor

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
require "json"
77
require "optparse"
8+
require "yaml"
89

910
module AgentWorkflowSeamDoctor
1011
SECTION = "Agent Workflow Configuration"
@@ -28,6 +29,30 @@ module AgentWorkflowSeamDoctor
2829
"Approval-exempt change categories",
2930
"Coordination backend"
3031
].freeze
32+
REQUIRED_CONFIG_KEYS = %w[
33+
base_branch
34+
hosted_ci_trigger
35+
ci_parity_environment
36+
secret_redaction_patterns
37+
benchmark_labels
38+
follow_up_prefix
39+
changelog
40+
merge_ledger
41+
review_gate
42+
approval_exempt
43+
coordination_backend
44+
].freeze
45+
REQUIRED_COMMAND_SCRIPTS = %w[
46+
setup
47+
validate
48+
test
49+
lint
50+
build
51+
docs
52+
ci-detect
53+
].freeze
54+
CONFIG_RELATIVE_PATH = ".agents/agent-workflow.yml"
55+
COMMAND_README_RELATIVE_PATH = ".agents/bin/README.md"
3156
CONFIG_KEY_PATTERN = /^-\s+\*\*(.+?)\*\*:\s*(.*)$/
3257

3358
SEAM_PLACEHOLDER = %r{
@@ -84,10 +109,16 @@ module AgentWorkflowSeamDoctor
84109

85110
# Read as UTF-8 regardless of locale; a non-UTF-8 default external encoding
86111
# (e.g. LANG=C) otherwise crashes the parser on non-ASCII bytes in AGENTS.md.
87-
config = parse_config(File.binread(agents_path).force_encoding("UTF-8").scrub)
88-
if config.nil?
112+
agents_text = File.binread(agents_path).force_encoding("UTF-8").scrub
113+
section = extract_section(agents_text)
114+
if section.nil?
89115
issues << "missing AGENTS.md section: #{SECTION}"
116+
elsif File.file?(File.join(root, CONFIG_RELATIVE_PATH))
117+
issues.concat(portable_contract_issues(section))
118+
issues.concat(command_script_issues(root))
119+
issues.concat(yaml_config_issues(root))
90120
else
121+
config = parse_config(agents_text)
91122
issues.concat(missing_key_issues(config))
92123
issues.concat(unresolved_extra_key_issues(config))
93124
end
@@ -101,6 +132,86 @@ module AgentWorkflowSeamDoctor
101132
issues
102133
end
103134

135+
def portable_contract_issues(section)
136+
issues = []
137+
unless section.include?(COMMAND_README_RELATIVE_PATH) && section.include?(".agents/bin/<name>")
138+
issues << "AGENTS.md section must point to #{COMMAND_README_RELATIVE_PATH} and .agents/bin/<name>"
139+
end
140+
141+
unless section.include?(CONFIG_RELATIVE_PATH)
142+
issues << "AGENTS.md section must point to #{CONFIG_RELATIVE_PATH}"
143+
end
144+
145+
issues
146+
end
147+
148+
def command_script_issues(root)
149+
REQUIRED_COMMAND_SCRIPTS.flat_map do |script_name|
150+
path = File.join(root, ".agents/bin", script_name)
151+
if !File.file?(path)
152+
["missing agent workflow command script: .agents/bin/#{script_name}"]
153+
elsif !File.executable?(path)
154+
["agent workflow command script is not executable: .agents/bin/#{script_name}"]
155+
else
156+
[]
157+
end
158+
end
159+
end
160+
161+
def yaml_config_issues(root)
162+
path = File.join(root, CONFIG_RELATIVE_PATH)
163+
config = load_yaml_config(path)
164+
return config if config.is_a?(Array)
165+
166+
issues = REQUIRED_CONFIG_KEYS.filter_map do |key|
167+
value = config[key]
168+
if value.nil?
169+
"missing agent workflow config key: #{key}"
170+
elsif unresolved_yaml_value?(value)
171+
"unresolved agent workflow config value for key: #{key}"
172+
end
173+
end
174+
175+
if config["follow_up_prefix"] != "Follow-up:"
176+
issues << 'invalid agent workflow config value for key: follow_up_prefix (expected "Follow-up:")'
177+
end
178+
179+
config.each do |key, value|
180+
next if REQUIRED_CONFIG_KEYS.include?(key)
181+
next unless unresolved_yaml_value?(value)
182+
183+
issues << "unresolved agent workflow config value for key: #{key}"
184+
end
185+
186+
issues
187+
end
188+
189+
def load_yaml_config(path)
190+
parsed = YAML.safe_load(File.read(path), permitted_classes: [], permitted_symbols: [], aliases: false)
191+
return ["#{CONFIG_RELATIVE_PATH} must be a mapping"] unless parsed.is_a?(Hash)
192+
193+
parsed
194+
rescue Psych::SyntaxError => e
195+
["invalid #{CONFIG_RELATIVE_PATH}: #{e.message}"]
196+
rescue Errno::ENOENT
197+
["missing #{CONFIG_RELATIVE_PATH}"]
198+
end
199+
200+
def unresolved_yaml_value?(value)
201+
case value
202+
when String
203+
unresolved_template_value?(value)
204+
when Array
205+
value.empty? || value.any? { |item| unresolved_yaml_value?(item) }
206+
when Hash
207+
value.empty? || value.any? { |_key, item| unresolved_yaml_value?(item) }
208+
when NilClass
209+
true
210+
else
211+
false
212+
end
213+
end
214+
104215
def parse_config(text)
105216
section = extract_section(text)
106217
return nil if section.nil?

.agents/bin/agent-workflow-seam-doctor-test.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,52 @@ def with_repo
2121
Dir.mktmpdir("agent-workflow-seam-doctor-test") do |dir|
2222
FileUtils.mkdir_p(File.join(dir, ".agents/skills/example"))
2323
FileUtils.mkdir_p(File.join(dir, ".agents/workflows"))
24+
FileUtils.mkdir_p(File.join(dir, ".agents/bin"))
2425
yield dir
2526
end
2627
end
2728

29+
def write_portable_contract(root)
30+
body = +"# AGENTS.md\n\n"
31+
body << "## Agent Workflow Configuration\n\n"
32+
body << "Portable shared skills resolve this repo's commands and policy through:\n\n"
33+
body << "- **Commands** — run `.agents/bin/<name>` (`setup`, `validate`, `test`, `lint`, "
34+
body << "`build`, `docs`, `ci-detect`); see [`.agents/bin/README.md`](.agents/bin/README.md).\n"
35+
body << "- **Policy / config** — [`.agents/agent-workflow.yml`](.agents/agent-workflow.yml).\n"
36+
body << "\n## Commands\n"
37+
File.write(File.join(root, "AGENTS.md"), body)
38+
write_agent_workflow_config(root)
39+
write_agent_workflow_scripts(root)
40+
end
41+
42+
def write_agent_workflow_config(root, overrides = {})
43+
config = {
44+
"base_branch" => "main",
45+
"hosted_ci_trigger" => "+ci-* PR-comment commands",
46+
"ci_parity_environment" => "No dedicated act/local runner image; use bin/ci-local.",
47+
"secret_redaction_patterns" => "Redact SECRET, TOKEN, KEY, PASSWORD, and LICENSE.",
48+
"benchmark_labels" => "benchmark, benchmark-core",
49+
"follow_up_prefix" => "Follow-up:",
50+
"changelog" => "/CHANGELOG.md, user-visible changes only.",
51+
"merge_ledger" => "script/pr-merge-ledger <PR> --strict",
52+
"review_gate" => "claude-review",
53+
"approval_exempt" => "focused trusted workflow/build/dependency edits",
54+
"coordination_backend" => "private shakacode/agent-coordination"
55+
}.merge(overrides)
56+
57+
File.write(File.join(root, ".agents/agent-workflow.yml"), YAML.dump(config))
58+
end
59+
60+
def write_agent_workflow_scripts(root, omit: [])
61+
AgentWorkflowSeamDoctor::REQUIRED_COMMAND_SCRIPTS.each do |script_name|
62+
next if omit.include?(script_name)
63+
64+
path = File.join(root, ".agents/bin", script_name)
65+
File.write(path, "#!/usr/bin/env bash\nexit 0\n")
66+
FileUtils.chmod("+x", path)
67+
end
68+
end
69+
2870
def write_agents(root, seam = REQUIRED_SEAM)
2971
body = +"# AGENTS.md\n\n"
3072
body << "## Agent Workflow Configuration\n\n"
@@ -51,6 +93,73 @@ def run_doctor(root, *)
5193
class AgentWorkflowSeamDoctorConfigTest < Minitest::Test
5294
include AgentWorkflowSeamDoctorTestHelpers
5395

96+
def test_portable_contract_without_inline_keys_passes
97+
with_repo do |root|
98+
write_portable_contract(root)
99+
write_skill(root, "No commands here.\n")
100+
101+
out, status = run_doctor(root)
102+
103+
assert status.success?, out
104+
assert_includes out, "PASS"
105+
end
106+
end
107+
108+
def test_portable_contract_missing_script_fails
109+
with_repo do |root|
110+
write_portable_contract(root)
111+
FileUtils.rm_f(File.join(root, ".agents/bin/lint"))
112+
write_skill(root, "No commands here.\n")
113+
114+
out, status = run_doctor(root)
115+
116+
refute status.success?
117+
assert_includes out, "missing agent workflow command script: .agents/bin/lint"
118+
end
119+
end
120+
121+
def test_portable_contract_invalid_yaml_fails
122+
with_repo do |root|
123+
write_portable_contract(root)
124+
File.write(File.join(root, ".agents/agent-workflow.yml"), "base_branch: [\n")
125+
write_skill(root, "No commands here.\n")
126+
127+
out, status = run_doctor(root)
128+
129+
refute status.success?
130+
assert_includes out, "invalid .agents/agent-workflow.yml"
131+
end
132+
end
133+
134+
def test_portable_contract_missing_yaml_key_fails
135+
with_repo do |root|
136+
write_portable_contract(root)
137+
write_agent_workflow_config(root, "follow_up_prefix" => nil)
138+
write_skill(root, "No commands here.\n")
139+
140+
out, status = run_doctor(root)
141+
142+
refute status.success?
143+
assert_includes out, "missing agent workflow config key: follow_up_prefix"
144+
end
145+
end
146+
147+
def test_portable_contract_follow_up_prefix_must_be_literal_prefix
148+
with_repo do |root|
149+
write_portable_contract(root)
150+
write_agent_workflow_config(
151+
root,
152+
"follow_up_prefix" => "Follow-up: (default to no new issue; see policy)"
153+
)
154+
write_skill(root, "No commands here.\n")
155+
156+
out, status = run_doctor(root)
157+
158+
refute status.success?
159+
assert_includes out, "invalid agent workflow config value for key: follow_up_prefix"
160+
end
161+
end
162+
54163
def test_complete_seam_without_executable_placeholders_passes
55164
with_repo do |root|
56165
write_agents(root)

.agents/bin/lint

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
set -euo pipefail
44
cd "$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
55
( cd react_on_rails && bundle exec rake lint )
6+
pnpm run lint
7+
pnpm start format.listDifferent

.agents/bin/setup

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# Install dependencies.
33
set -euo pipefail
44
cd "$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
5-
exec script/bootstrap
5+
exec bin/setup

.agents/bin/test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
2-
# Run tests (Ruby gem specs + JS).
2+
# Run tests (Ruby except generated examples + JS).
33
set -euo pipefail
44
cd "$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
5-
( cd react_on_rails && bundle exec rake run_rspec )
5+
( cd react_on_rails && bundle exec rake run_rspec:all_but_examples )
66
pnpm run test

0 commit comments

Comments
 (0)