Skip to content

Commit 65841d2

Browse files
justin808claude
andauthored
Add downstream seam sync command and registry (#22)
* Add downstream seam sync command and registry Add bin/push-downstream and downstream.yml so the managed "## Agent Workflow Configuration" seam can be rolled into consumer repos as one PR per repo, while each repo's seam values stay repo-owned. - reuse AgentWorkflowSeamDoctor::REQUIRED_KEYS and parsing as the single source of truth, so the command and the seam doctor cannot drift - reconcile is idempotent: insert a base-branch-seeded, n/a-filled seam when absent; preserve existing values (including multi-line) and extra optional keys when present - registry mode plans by default and clones/reconciles/validates/opens PRs under --apply; --root reconciles a local checkout with no network - seed downstream.yml with the 16 public consumer repos (react_on_rails is the reference seam; private and archived repos are out of scope) - wire bin/push-downstream-test.rb and a registry dry-run into bin/validate - document the workflow in docs/downstream-sync.md and the README Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Create AGENTS.md when missing in downstream sync Adopt repos that have no AGENTS.md yet by creating a minimal one (title plus the managed seam) instead of erroring. Both --root mode and registry --apply now create-or-update via a shared reconcile_agents helper; --root also fails cleanly when the target directory does not exist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Read AGENTS.md as UTF-8 in downstream sync Real AGENTS.md files carry non-ASCII bytes (em dashes, arrows). Under a non-UTF-8 default external encoding (e.g. LANG=C, common in headless agents), File.read returned a US-ASCII string and crashed match? on the first non-ASCII line. Read via binread + force_encoding("UTF-8").scrub, matching agent-workflow-seam-doctor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add three-layer seam value adapter Seed seam values through defaults -> preset -> per-repo overrides so the fan-out renders complete, agent-optimized seams instead of n/a scaffolds. - seam-presets.yml: org-uniform defaults + archetype presets (ts-package, ruby-gem, ror-demo, site) - downstream.yml: per-repo `preset:` + optional `overrides:` (RSC carries the NODE_CONDITIONS override and is disabled pending its canary PR) - resolve_values layers the three sources and seeds the base branch; unknown presets fail before any writes - reconcile(..., seed:) fills unset keys from the seed while repo-owned values still win, so re-runs stay idempotent - registry plan shows the chosen preset; --presets selects the preset file Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address downstream sync review feedback * Handle existing downstream sync branches * Preserve downstream sync branch reruns * Harden downstream sync branch reruns * Reuse existing downstream sync branches * Skip PR creation for synced downstream branches * Validate current downstream seams * Assert downstream sync branch preconditions * Rework downstream sync around binstub contract Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix upgrade validation with no consumers * Address downstream sync review followups * Clarify workflow policy sources --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 156d4eb commit 65841d2

36 files changed

Lines changed: 2494 additions & 713 deletions

README.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Reusable agent workflow skills for ShakaCode repositories.
55
This repository contains portable Codex/Claude-facing workflows for PR batches,
66
review triage, merge readiness, changelog updates, CI routing, and audit loops.
77
The shared files provide process. Each adopting repository keeps its concrete
8-
commands and policy in `AGENTS.md` under `## Agent Workflow Configuration`.
8+
commands in `.agents/bin/` and non-command policy in `.agents/agent-workflow.yml`.
9+
Its `AGENTS.md` has a short pointer section named `## Agent Workflow Configuration`.
910

1011
## Why This Exists
1112

@@ -15,8 +16,8 @@ it. The default model is:
1516

1617
- install this shared workflow pack once in the user's or agent's normal skill
1718
home;
18-
- add a small, repo-owned seam to each consumer repo's `AGENTS.md`;
19-
- validate that installed workflows can resolve the consumer repo's seam;
19+
- add repo-owned `.agents/bin/` wrappers and `.agents/agent-workflow.yml`;
20+
- validate that installed workflows can resolve the consumer repo's contract;
2021
- keep repo-specific skills and overrides in the consumer repo only when needed.
2122

2223
This is deliberately not a subtree-first model. Repos may pin local copies when
@@ -29,7 +30,9 @@ plus a validated repo seam are the default.
2930
| --- | --- |
3031
| `skills/` | Agent skill folders. Copy or symlink these under a Codex or Claude skill root. |
3132
| `workflows/` | Longer workflow prompts and shared operating models referenced by skills. |
32-
| `bin/` | Install, status, upgrade, and validation helpers. |
33+
| `bin/` | Install, status, upgrade, validation, and downstream-sync helpers. |
34+
| `downstream.yml` | Registry of consumer repos for `bin/push-downstream`. |
35+
| `seam-presets.yml` | Seam value adapter: org defaults + archetype presets. |
3336
| `docs/` | Adoption, seam design, and operator guidance. |
3437
| `examples/` | Example consumer-repo configuration snippets. |
3538
| `test/fixtures/consumer-repo/` | Minimal fixture used by `bin/validate`. |
@@ -80,11 +83,12 @@ notes.
8083

8184
In each repository that should use these workflows:
8285

83-
1. Add or update `AGENTS.md`.
84-
2. Add an `## Agent Workflow Configuration` section with the real repo values.
85-
3. Add repo-local skills only for domain-specific workflows or intentional
86+
1. Add or update `.agents/bin/` command wrappers.
87+
2. Add `.agents/agent-workflow.yml` with the repo's non-command policy.
88+
3. Add the `## Agent Workflow Configuration` pointer section to `AGENTS.md`.
89+
4. Add repo-local skills only for domain-specific workflows or intentional
8690
overrides.
87-
4. Validate the seam from the consumer repo:
91+
5. Validate the contract from the consumer repo:
8892

8993
```bash
9094
agent-workflow-seam-doctor --shared "$HOME/src/agent-workflows"
@@ -98,14 +102,29 @@ In each repository that should use these workflows:
98102
--shared "$HOME/src/agent-workflows"
99103
```
100104

101-
5. Dry-run one workflow, such as `$plan-pr-batch` or `$address-review`, without
105+
6. Dry-run one workflow, such as `$plan-pr-batch` or `$address-review`, without
102106
making code changes.
103107

104108
See [docs/adoption.md](docs/adoption.md) for the full adoption guide,
105109
[docs/seam-design.md](docs/seam-design.md) for the design rationale, and
106110
[docs/installation-and-upgrades.md](docs/installation-and-upgrades.md) for
107111
ongoing host installs and upgrades.
108112

113+
## Downstream Seam Sync
114+
115+
`bin/push-downstream` rolls the binstub contract into the consumer repos listed
116+
in `downstream.yml`, one PR per repo, while preserving repo-owned scripts and
117+
policy values. Plan first, then apply a canary before fanning out:
118+
119+
```bash
120+
bin/push-downstream # plan every enabled repo
121+
bin/push-downstream --only shakapacker --apply # clone, reconcile, validate, open one PR
122+
bin/push-downstream --apply # fan out to all enabled repos
123+
```
124+
125+
See [docs/downstream-sync.md](docs/downstream-sync.md) for the registry schema,
126+
the managed-vs-repo-owned boundary, and `--root`/`--only`/`--all` usage.
127+
109128
## Skill Inventory
110129

111130
| Skill | Use |

bin/agent-workflow-seam-doctor

Lines changed: 140 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,39 @@
44
# Validate that portable agent workflows can resolve their repo-specific seam.
55

66
require "json"
7+
require "open3"
78
require "optparse"
9+
require "yaml"
810

911
module AgentWorkflowSeamDoctor
1012
SECTION = "Agent Workflow Configuration"
1113
SECTION_HEADING = /^##\s+#{Regexp.escape(SECTION)}\s*$/
12-
REQUIRED_KEYS = [
13-
"Base branch",
14-
"Pre-push local validation",
15-
"CI change detector",
16-
"Hosted-CI trigger",
17-
"CI parity environment",
18-
"Benchmark labels",
19-
"Follow-up issue prefix",
20-
"Changelog",
21-
"Lint / format",
22-
"Merge ledger",
23-
"Docs checks",
24-
"Tests",
25-
"Build / type checks",
26-
"Review gate",
27-
"Approval-exempt change categories",
28-
"Coordination backend"
14+
POINTER_SECTION = <<~MARKDOWN.chomp
15+
## Agent Workflow Configuration
16+
17+
Portable shared skills resolve this repo's commands and policy through:
18+
- **Commands** — run `.agents/bin/<name>` (`setup`, `validate`, `test`, ...); see `.agents/bin/README.md`. A missing script means that capability is n/a here.
19+
- **Policy / config** — `.agents/agent-workflow.yml`.
20+
MARKDOWN
21+
COMMANDS_DIR = ".agents/bin"
22+
COMMAND_README = ".agents/bin/README.md"
23+
POLICY_CONFIG = ".agents/agent-workflow.yml"
24+
STANDARD_SCRIPTS = %w[setup validate test lint build docs ci-detect].freeze
25+
CORE_SCRIPTS = %w[validate test].freeze
26+
ROOT_RESOLUTION = 'CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd'
27+
REQUIRED_POLICY_KEYS = %w[
28+
base_branch
29+
follow_up_prefix
30+
review_gate
31+
approval_exempt
32+
coordination_backend
33+
changelog
34+
benchmark_labels
35+
merge_ledger
36+
ci_parity_environment
37+
hosted_ci_trigger
38+
ci_change_detector
2939
].freeze
30-
CONFIG_KEY_PATTERN = /^-\s+\*\*(.+?)\*\*:\s*(.*)$/
31-
3240
SEAM_PLACEHOLDER = %r{
3341
<[^>\n]*
3442
(?:
@@ -76,13 +84,10 @@ module AgentWorkflowSeamDoctor
7684

7785
# Read as UTF-8 regardless of locale; a non-UTF-8 default external encoding
7886
# (e.g. LANG=C) otherwise crashes the parser on non-ASCII bytes in AGENTS.md.
79-
config = parse_config(File.binread(agents_path).force_encoding("UTF-8").scrub)
80-
if config.nil?
81-
issues << "missing AGENTS.md section: #{SECTION}"
82-
else
83-
issues.concat(missing_key_issues(config))
84-
issues.concat(unresolved_extra_key_issues(config))
85-
end
87+
agents_text = File.binread(agents_path).force_encoding("UTF-8").scrub
88+
issues.concat(pointer_section_issues(agents_text))
89+
issues.concat(binstub_issues(root))
90+
issues.concat(policy_issues(root))
8691

8792
issues.concat(shared_root_issues(shared_roots))
8893

@@ -93,36 +98,127 @@ module AgentWorkflowSeamDoctor
9398
issues
9499
end
95100

96-
def parse_config(text)
101+
def pointer_section_issues(text)
97102
section = extract_section(text)
98-
return nil if section.nil?
103+
if section.nil?
104+
return ["missing AGENTS.md section: #{SECTION}"]
105+
end
99106

100-
config = {}
101-
current_key = nil
107+
actual = "## #{SECTION}\n#{section}".rstrip
108+
return [] if actual == POINTER_SECTION
102109

103-
section.each_line do |line|
104-
if (match = line.match(CONFIG_KEY_PATTERN))
105-
current_key = match[1].strip
106-
config[current_key] = match[2].strip
107-
next
110+
["AGENTS.md section does not match binstub pointer: #{SECTION}"]
111+
end
112+
113+
def binstub_issues(root)
114+
issues = []
115+
commands_dir = File.join(root, COMMANDS_DIR)
116+
unless File.directory?(commands_dir)
117+
return ["missing commands directory: #{COMMANDS_DIR}"]
118+
end
119+
120+
readme = File.join(root, COMMAND_README)
121+
issues << "missing commands README: #{COMMAND_README}" unless File.file?(readme)
122+
123+
CORE_SCRIPTS.each do |script|
124+
path = File.join(root, COMMANDS_DIR, script)
125+
unless File.file?(path)
126+
issues << "missing core script: #{COMMANDS_DIR}/#{script}"
108127
end
128+
end
129+
130+
script_names = STANDARD_SCRIPTS.select { |script| File.file?(File.join(root, COMMANDS_DIR, script)) }
131+
script_names.each do |script|
132+
issues.concat(script_issues(root, script))
133+
end
134+
135+
issues
136+
end
137+
138+
def script_issues(root, script)
139+
relative = "#{COMMANDS_DIR}/#{script}"
140+
path = File.join(root, relative)
141+
issues = []
142+
143+
unless File.executable?(path)
144+
issues << if CORE_SCRIPTS.include?(script)
145+
"core script is not executable: #{relative}"
146+
else
147+
"script is not executable: #{relative}"
148+
end
149+
end
150+
151+
content = File.binread(path).force_encoding("UTF-8").scrub
152+
issues << "script is not a bash wrapper: #{relative}" unless content.start_with?("#!/usr/bin/env bash\n")
153+
issues << "script does not enable strict bash mode: #{relative}" unless content.include?("set -euo pipefail")
154+
issues << "script does not cd to repo root: #{relative}" unless script_cd_to_root?(content)
155+
issues.concat(missing_sibling_script_issues(root, relative, content))
156+
157+
_out, status = Open3.capture2e("bash", "-n", path)
158+
issues << "script has bash syntax error: #{relative}" unless status.success?
159+
160+
issues
161+
end
162+
163+
def missing_sibling_script_issues(root, relative, content)
164+
content.scan(%r{\$(?:root|\{root\})/\.agents/bin/([A-Za-z0-9_.-]+)}).flatten.uniq.filter_map do |script|
165+
next if File.file?(File.join(root, COMMANDS_DIR, script))
109166

110-
if config_continuation?(line, current_key)
111-
config[current_key] = [config[current_key], line.strip].reject(&:empty?).join(" ")
112-
elsif config_key_finished?(line)
113-
current_key = nil
167+
"script references missing sibling script: #{relative} -> #{COMMANDS_DIR}/#{script}"
168+
end
169+
end
170+
171+
def script_cd_to_root?(content)
172+
return false unless content.include?(ROOT_RESOLUTION)
173+
174+
content.include?('cd "$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"') ||
175+
content.include?('cd "$root"') ||
176+
content.include?('cd "${root}"')
177+
end
178+
179+
def policy_issues(root)
180+
path = File.join(root, POLICY_CONFIG)
181+
return ["missing policy config: #{POLICY_CONFIG}"] unless File.file?(path)
182+
183+
config = load_policy_config(path)
184+
return ["invalid policy config: #{POLICY_CONFIG}"] unless config.is_a?(Hash)
185+
186+
issues = []
187+
REQUIRED_POLICY_KEYS.each do |key|
188+
if !config.key?(key)
189+
issues << "missing policy key: #{key}"
190+
elsif unresolved_policy_value?(config[key])
191+
issues << "unresolved policy value for key: #{key}"
114192
end
115193
end
116194

117-
config
195+
config.each do |key, value|
196+
next if REQUIRED_POLICY_KEYS.include?(key)
197+
next unless unresolved_policy_value?(value)
198+
199+
issues << "unresolved policy value for key: #{key}"
200+
end
201+
202+
issues
203+
rescue Psych::Exception
204+
["invalid policy config: #{POLICY_CONFIG}"]
118205
end
119206

120-
def config_continuation?(line, current_key)
121-
current_key && line.match?(/^\s{2,}\S/) && !line.match?(CONFIG_KEY_PATTERN)
207+
def load_policy_config(path)
208+
YAML.safe_load(File.read(path, encoding: "UTF-8"), aliases: false) || {}
122209
end
123210

124-
def config_key_finished?(line)
125-
line.strip.empty? || !line.match?(/^\s{2,}\S/)
211+
def unresolved_policy_value?(value)
212+
case value
213+
when String
214+
unresolved_template_value?(value)
215+
when Array
216+
value.empty? || value.any? { |entry| unresolved_policy_value?(entry) }
217+
when Hash
218+
value.empty? || value.any? { |key, entry| unresolved_policy_value?(key.to_s) || unresolved_policy_value?(entry) }
219+
else
220+
value.nil?
221+
end
126222
end
127223

128224
def extract_section(text)
@@ -139,26 +235,6 @@ module AgentWorkflowSeamDoctor
139235
body.join
140236
end
141237

142-
def missing_key_issues(config)
143-
REQUIRED_KEYS.filter_map do |key|
144-
value = config[key]
145-
if value.nil?
146-
"missing #{SECTION} key: #{key}"
147-
elsif unresolved_template_value?(value)
148-
"unresolved #{SECTION} value for key: #{key}"
149-
end
150-
end
151-
end
152-
153-
def unresolved_extra_key_issues(config)
154-
config.filter_map do |key, value|
155-
next if REQUIRED_KEYS.include?(key)
156-
next unless unresolved_template_value?(value)
157-
158-
"unresolved #{SECTION} value for key: #{key}"
159-
end
160-
end
161-
162238
def unresolved_template_value?(value)
163239
stripped = value.strip
164240
stripped.empty? || stripped.match?(SEAM_PLACEHOLDER) || stripped.match?(CI_PARITY_SEAM_PLACEHOLDER)

0 commit comments

Comments
 (0)