jekyll-theme-zer0 1.20.2 → 1.21.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/_data/backlog.yml +7 -0
- data/_data/roadmap_plan.yml +37 -0
- data/_data/routing.yml +73 -0
- data/scripts/sync-backlog.rb +149 -26
- data/scripts/sync-plan.rb +164 -0
- data/scripts/sync-plan.sh +21 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8552bc1992ed59b6f63392207f6a49d12bc9e66ac16cd43b61bb57700f45395e
|
|
4
|
+
data.tar.gz: 54304e964cff2dc7fafe3ab03c722dfac994011994be96201d700a67e224ec35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d656b64ce2440c13047ce0802ca968c767cbcfe20eb8b442973e5cc0a63aafb29263081f2ab5c5737260775643193e9fad9eaa08680fe0151b8442efae15128d
|
|
7
|
+
data.tar.gz: a1a9000773b53140084dd986072d1cbfacda99ba66859cfd5cd82965e163b1aa47184ef64e56cb41ed0a6e0ae2950a96a5f4935e637f073c756de757afb36a53
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.21.0](https://github.com/bamr87/zer0-mistakes/compare/v1.20.2...v1.21.0) (2026-06-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **automation:** committee planning + specialized executor agents (Phase 4) ([#226](https://github.com/bamr87/zer0-mistakes/issues/226)) ([1e9016b](https://github.com/bamr87/zer0-mistakes/commit/1e9016b869ff33f26fc60d3ae27e7fcd0216e802))
|
|
14
|
+
* **automation:** issue intake + /issue-implement routing (Phases 2-3) ([#225](https://github.com/bamr87/zer0-mistakes/issues/225)) ([21a7008](https://github.com/bamr87/zer0-mistakes/commit/21a70080b2814827bd390201e1ec76fb2b9a874e))
|
|
15
|
+
* **scripts:** issue adoption in sync-backlog (Phase 1) ([#224](https://github.com/bamr87/zer0-mistakes/issues/224)) ([512068e](https://github.com/bamr87/zer0-mistakes/commit/512068e2556be7fe3a4c7870d9feab0ca52955d8))
|
|
16
|
+
|
|
8
17
|
## [1.20.2](https://github.com/bamr87/zer0-mistakes/compare/v1.20.1...v1.20.2) (2026-06-25)
|
|
9
18
|
|
|
10
19
|
|
data/_data/backlog.yml
CHANGED
|
@@ -44,8 +44,15 @@
|
|
|
44
44
|
# summary: 1–2 lines of context
|
|
45
45
|
# acceptance: List of checkable done-criteria the implement routine verifies
|
|
46
46
|
# links: { issue: <#|null>, pr: <#|null>, roadmap: "<version>|null" }
|
|
47
|
+
# When `issue:` points at an EXISTING issue, sync ADOPTS it (adds
|
|
48
|
+
# a managed block, preserves the author's text, no duplicate)
|
|
49
|
+
# instead of creating a new one. At most one task per issue.
|
|
47
50
|
# created: YYYY-MM-DD
|
|
48
51
|
# updated: YYYY-MM-DD
|
|
52
|
+
# route: (optional) executor-lane hint for /issue-implement; resolved via
|
|
53
|
+
# _data/routing.yml (defaults to the lane for `area`).
|
|
54
|
+
# depends_on: (optional) [T-NNN, …] tasks that must land first; consumed by the
|
|
55
|
+
# /issue-plan committee for ordering. Advisory.
|
|
49
56
|
#
|
|
50
57
|
# `risk: low` + area in {docs, deps, lint} makes a task auto-merge eligible once
|
|
51
58
|
# CI is green (see `.github/prompts/backlog-implement.prompt.md`). Everything
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Roadmap plan — ORDER-ONLY sequencing artifact
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Produced by the /issue-plan committee (.github/prompts/issue-plan.prompt.md).
|
|
5
|
+
# Validated + mirrored by scripts/sync-plan.rb.
|
|
6
|
+
#
|
|
7
|
+
# This file sequences OPEN backlog tasks into batches. It is ADVISORY input to
|
|
8
|
+
# the human-dispatched /issue-implement ordering — it does not dispatch anything.
|
|
9
|
+
#
|
|
10
|
+
# HARD INVARIANT (enforced by `sync-plan.rb --check`): order only. A batch may
|
|
11
|
+
# carry ONLY `id`, `goal`, `tasks` (bare T-NNN ids that exist + are open),
|
|
12
|
+
# `depends_on` (other batch ids; the DAG must be acyclic), and `test_framework`.
|
|
13
|
+
# It may NEVER carry risk / priority / area / status / effort / acceptance —
|
|
14
|
+
# those are DERIVED at read-time from `_data/backlog.yml` (the single source of
|
|
15
|
+
# truth), exactly as /backlog-implement already does. Two sources of truth drift.
|
|
16
|
+
#
|
|
17
|
+
# Example batch shape:
|
|
18
|
+
# batches:
|
|
19
|
+
# - id: B-1
|
|
20
|
+
# goal: "Unblock remote-theme consumers"
|
|
21
|
+
# tasks: [T-027, T-031]
|
|
22
|
+
# depends_on: []
|
|
23
|
+
# test_framework: "Playwright smoke for the 404 path + a remote-theme build matrix job"
|
|
24
|
+
# - id: B-2
|
|
25
|
+
# goal: "Theme chrome a11y pass"
|
|
26
|
+
# tasks: [T-033]
|
|
27
|
+
# depends_on: [B-1]
|
|
28
|
+
# test_framework: "axe-core checks + before/after visual evidence per the visual-evidence skill"
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
meta:
|
|
32
|
+
title: "zer0-mistakes Roadmap Plan"
|
|
33
|
+
updated: 2026-06-25
|
|
34
|
+
generated_by: "/issue-plan"
|
|
35
|
+
|
|
36
|
+
# Populated by the committee. Empty is valid (nothing sequenced yet).
|
|
37
|
+
batches: []
|
data/_data/routing.yml
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Issue routing — area:* (+ optional path globs) → executor lane
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Used by /issue-implement to pick the specialized executor for a task/issue.
|
|
5
|
+
#
|
|
6
|
+
# Resolution order:
|
|
7
|
+
# 1. explicit task.route -> that lane (override)
|
|
8
|
+
# 2. first `rules` entry whose `area` matches the task's area AND (if it lists
|
|
9
|
+
# `paths`) a path glob matches a file the work is expected to touch
|
|
10
|
+
# 3. `default` lane
|
|
11
|
+
#
|
|
12
|
+
# A "lane" pairs a specialized .claude/agents/<agent>.md executor with the
|
|
13
|
+
# file-scoped instructions + skills it must load. In v1 (until the cloud-routine
|
|
14
|
+
# substrate spike proves prompt->subagent dispatch), /issue-implement loads the
|
|
15
|
+
# lane's `instructions` + `skills` INLINE and the named `agent` is advisory; once
|
|
16
|
+
# the spike passes it delegates to the agent. Either way the lane is the same.
|
|
17
|
+
#
|
|
18
|
+
# NOTE: this file is DATA. It never grants autonomy — risk/auto-merge eligibility
|
|
19
|
+
# is derived from the backlog task per the autonomy policy in
|
|
20
|
+
# docs/systems/continuous-evolution.md, not from the lane.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
default: code-fixer
|
|
24
|
+
|
|
25
|
+
lanes:
|
|
26
|
+
content:
|
|
27
|
+
agent: content-reviewer
|
|
28
|
+
instructions: [content-review.instructions.md, documentation.instructions.md]
|
|
29
|
+
skills: [content-review, validate-build]
|
|
30
|
+
done: "content builds; deterministic + AI content review clean"
|
|
31
|
+
theme-ui:
|
|
32
|
+
agent: theme-ui
|
|
33
|
+
instructions: [layouts.instructions.md, includes.instructions.md, sass.instructions.md, visual-evidence.instructions.md]
|
|
34
|
+
skills: [change-workflow, visual-evidence, validate-build]
|
|
35
|
+
done: "jekyll build green; regression spec + before/after evidence committed"
|
|
36
|
+
code-fixer:
|
|
37
|
+
agent: code-fixer
|
|
38
|
+
instructions: [includes.instructions.md, layouts.instructions.md, scripts.instructions.md]
|
|
39
|
+
skills: [change-workflow, validate-build]
|
|
40
|
+
done: "targeted tests green; jekyll build green if templates touched"
|
|
41
|
+
infra-scripts:
|
|
42
|
+
agent: infra-scripts
|
|
43
|
+
instructions: [scripts.instructions.md]
|
|
44
|
+
skills: [change-workflow, validate-build]
|
|
45
|
+
done: "scripts run; shellcheck/tests green; no CODEOWNERS-owned path touched"
|
|
46
|
+
test-author:
|
|
47
|
+
agent: test-author
|
|
48
|
+
instructions: [testing.instructions.md, visual-evidence.instructions.md]
|
|
49
|
+
skills: [validate-build]
|
|
50
|
+
done: "new test fails before the fix and passes after; suite green"
|
|
51
|
+
a11y:
|
|
52
|
+
agent: a11y-fixer
|
|
53
|
+
instructions: [includes.instructions.md, layouts.instructions.md, sass.instructions.md, visual-evidence.instructions.md]
|
|
54
|
+
skills: [change-workflow, visual-evidence, validate-build]
|
|
55
|
+
done: "axe/a11y checks pass; regression spec + evidence committed"
|
|
56
|
+
deps:
|
|
57
|
+
agent: deps-bumper
|
|
58
|
+
instructions: []
|
|
59
|
+
skills: [change-workflow, validate-build]
|
|
60
|
+
done: "lockfiles consistent; build + tests green"
|
|
61
|
+
|
|
62
|
+
# First match wins. `feat` appears twice on purpose: a UI-touching feat routes to
|
|
63
|
+
# theme-ui, anything else falls through to the default code-fixer.
|
|
64
|
+
rules:
|
|
65
|
+
- { area: docs, lane: content }
|
|
66
|
+
- { area: a11y, lane: a11y }
|
|
67
|
+
- { area: tests, lane: test-author }
|
|
68
|
+
- { area: infra, lane: infra-scripts }
|
|
69
|
+
- { area: deps, lane: deps }
|
|
70
|
+
- { area: feat, paths: ["_layouts/**", "_includes/**", "_sass/**", "assets/**"], lane: theme-ui }
|
|
71
|
+
- { area: feat, lane: code-fixer }
|
|
72
|
+
- { area: perf, lane: code-fixer }
|
|
73
|
+
- { area: lint, lane: code-fixer }
|
data/scripts/sync-backlog.rb
CHANGED
|
@@ -89,6 +89,7 @@ def validate(data)
|
|
|
89
89
|
return ['Missing or empty `tasks:` list.'] unless tasks.is_a?(Array) && !tasks.empty?
|
|
90
90
|
|
|
91
91
|
seen_ids = {}
|
|
92
|
+
seen_links = {}
|
|
92
93
|
tasks.each_with_index do |task, i|
|
|
93
94
|
where = "tasks[#{i}]"
|
|
94
95
|
unless task.is_a?(Hash)
|
|
@@ -114,6 +115,30 @@ def validate(data)
|
|
|
114
115
|
unless task['acceptance'].is_a?(Array) && !task['acceptance'].empty?
|
|
115
116
|
errors << "#{where}: `acceptance` must be a non-empty list."
|
|
116
117
|
end
|
|
118
|
+
|
|
119
|
+
# Adoption guard: an existing GitHub issue may be claimed by at most one task,
|
|
120
|
+
# so two tasks can never fight over (or duplicate) the same issue.
|
|
121
|
+
lnk = task.dig('links', 'issue')
|
|
122
|
+
if lnk
|
|
123
|
+
unless lnk.is_a?(Integer) || lnk.to_s =~ /\A\d+\z/
|
|
124
|
+
errors << "#{where}: `links.issue` must be an issue number (got #{lnk.inspect})."
|
|
125
|
+
end
|
|
126
|
+
key = lnk.to_s
|
|
127
|
+
if seen_links[key]
|
|
128
|
+
errors << "#{where}: `links.issue` ##{key} already claimed by #{seen_links[key]}."
|
|
129
|
+
else
|
|
130
|
+
seen_links[key] = (id || where)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
# Optional routing/dependency metadata (consumed by /issue-implement + /issue-plan).
|
|
134
|
+
if task.key?('route') && !task['route'].is_a?(String)
|
|
135
|
+
errors << "#{where}: `route` must be a string."
|
|
136
|
+
end
|
|
137
|
+
if task.key?('depends_on') &&
|
|
138
|
+
!(task['depends_on'].is_a?(Array) &&
|
|
139
|
+
task['depends_on'].all? { |d| d.is_a?(String) && d =~ /\AT-\d{3,}\z/ })
|
|
140
|
+
errors << "#{where}: `depends_on` must be a list of T-NNN ids."
|
|
141
|
+
end
|
|
117
142
|
end
|
|
118
143
|
errors
|
|
119
144
|
end
|
|
@@ -133,6 +158,52 @@ def marker(id)
|
|
|
133
158
|
"<!-- backlog-id: #{id} -->"
|
|
134
159
|
end
|
|
135
160
|
|
|
161
|
+
# Delimiters around the sync-owned section of an ADOPTED human issue. Text ABOVE
|
|
162
|
+
# the start delimiter (the author's original report) is never touched; only this
|
|
163
|
+
# block is upserted on each sync, so adoption is non-destructive.
|
|
164
|
+
MANAGED_START = '<!-- backlog-managed:start -->'
|
|
165
|
+
MANAGED_END = '<!-- backlog-managed:end -->'
|
|
166
|
+
|
|
167
|
+
def render_managed_block(task)
|
|
168
|
+
accept = (task['acceptance'] || []).map { |a| "- [ ] #{a}" }.join("\n")
|
|
169
|
+
roadmap = task.dig('links', 'roadmap')
|
|
170
|
+
meta_row = [
|
|
171
|
+
"**Priority:** #{task['priority']}",
|
|
172
|
+
"**Area:** #{task['area']}",
|
|
173
|
+
"**Risk:** #{task['risk']}",
|
|
174
|
+
"**Effort:** #{task['effort']}",
|
|
175
|
+
"**Source:** #{task['source']}"
|
|
176
|
+
].join(' · ')
|
|
177
|
+
|
|
178
|
+
<<~BLOCK.strip
|
|
179
|
+
#{MANAGED_START}
|
|
180
|
+
#{marker(task['id'])}
|
|
181
|
+
> Tracked from [`_data/backlog.yml`](../blob/main/_data/backlog.yml) by `scripts/sync-backlog.rb`.
|
|
182
|
+
> The report above is the author's; this block is auto-managed — edit the backlog, not here.
|
|
183
|
+
|
|
184
|
+
#{meta_row}#{roadmap ? " · **Roadmap:** v#{roadmap}" : ''}
|
|
185
|
+
|
|
186
|
+
## Acceptance criteria
|
|
187
|
+
|
|
188
|
+
#{accept}
|
|
189
|
+
|
|
190
|
+
Picked up by the IMPLEMENT routine; see [`docs/systems/continuous-evolution.md`](../blob/main/docs/systems/continuous-evolution.md).
|
|
191
|
+
#{MANAGED_END}
|
|
192
|
+
BLOCK
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Upsert the managed block into a (human-authored) issue body, preserving the
|
|
196
|
+
# author's text. Idempotent: replaces an existing block, else appends one.
|
|
197
|
+
def upsert_managed_block(body, task)
|
|
198
|
+
body = body.to_s
|
|
199
|
+
block = render_managed_block(task)
|
|
200
|
+
if body.include?(MANAGED_START) && body.include?(MANAGED_END)
|
|
201
|
+
body.sub(/#{Regexp.escape(MANAGED_START)}.*#{Regexp.escape(MANAGED_END)}/m, block)
|
|
202
|
+
else
|
|
203
|
+
"#{body.rstrip}\n\n#{block}\n"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
136
207
|
def render_body(task)
|
|
137
208
|
accept = (task['acceptance'] || []).map { |a| "- [ ] #{a}" }.join("\n")
|
|
138
209
|
roadmap = task.dig('links', 'roadmap')
|
|
@@ -202,26 +273,52 @@ def ensure_labels(gh)
|
|
|
202
273
|
VALID_RISK.each { |r| gh.write(['label', 'create', "risk:#{r}", '--color', RISK_COLOR, '--force']) }
|
|
203
274
|
end
|
|
204
275
|
|
|
205
|
-
#
|
|
206
|
-
|
|
276
|
+
# Returns [index, link_state]:
|
|
277
|
+
# index — backlog id -> {number,state,labels,body,marker_id} for every
|
|
278
|
+
# marker-bearing agent-ready issue (the already-managed ones).
|
|
279
|
+
# link_state — issue number -> same record, for each issue referenced by a task
|
|
280
|
+
# `links.issue`. A human issue may carry neither the agent-ready
|
|
281
|
+
# label nor a marker yet, so it is fetched by number for adoption +
|
|
282
|
+
# provenance checks (is it already claimed by a foreign marker?).
|
|
283
|
+
def existing_issues(gh, link_numbers = [])
|
|
284
|
+
index = {}
|
|
285
|
+
link_state = {}
|
|
286
|
+
to_rec = lambda do |issue|
|
|
287
|
+
body = issue['body'].to_s
|
|
288
|
+
rec = {
|
|
289
|
+
'number' => issue['number'],
|
|
290
|
+
'state' => issue['state'].to_s.downcase,
|
|
291
|
+
'labels' => (issue['labels'] || []).map { |l| l['name'] },
|
|
292
|
+
'body' => body,
|
|
293
|
+
'marker_id' => body[/<!-- backlog-id: (T-\d+) -->/, 1]
|
|
294
|
+
}
|
|
295
|
+
index[rec['marker_id']] = rec if rec['marker_id']
|
|
296
|
+
rec
|
|
297
|
+
end
|
|
298
|
+
|
|
207
299
|
raw = gh.read(
|
|
208
300
|
['issue', 'list', '--label', 'agent-ready', '--state', 'all', '--limit', '500',
|
|
209
301
|
'--json', 'number,body,state,labels'],
|
|
210
302
|
default: '[]'
|
|
211
303
|
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
304
|
+
begin
|
|
305
|
+
JSON.parse(raw).each { |issue| to_rec.call(issue) }
|
|
306
|
+
rescue JSON::ParserError
|
|
307
|
+
# leave index empty; adoption/create still work off link_state
|
|
308
|
+
end
|
|
215
309
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
310
|
+
link_numbers.compact.map(&:to_i).uniq.each do |n|
|
|
311
|
+
raw1 = gh.read(['issue', 'view', n.to_s, '--json', 'number,body,state,labels'], default: '')
|
|
312
|
+
next if raw1.strip.empty?
|
|
313
|
+
|
|
314
|
+
begin
|
|
315
|
+
link_state[n] = to_rec.call(JSON.parse(raw1))
|
|
316
|
+
rescue JSON::ParserError
|
|
317
|
+
next
|
|
318
|
+
end
|
|
221
319
|
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{}
|
|
320
|
+
|
|
321
|
+
[index, link_state]
|
|
225
322
|
end
|
|
226
323
|
|
|
227
324
|
# ---------------------------------------------------------------------------
|
|
@@ -241,29 +338,55 @@ end
|
|
|
241
338
|
|
|
242
339
|
def sync(data, gh)
|
|
243
340
|
ensure_labels(gh)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
341
|
+
tasks = data['tasks'] || []
|
|
342
|
+
link_numbers = tasks.map { |t| t.dig('links', 'issue') }.compact
|
|
343
|
+
index, link_state = existing_issues(gh, link_numbers)
|
|
344
|
+
created = adopted = updated = closed = 0
|
|
345
|
+
|
|
346
|
+
tasks.each do |task|
|
|
347
|
+
id = task['id']
|
|
348
|
+
title = task['title']
|
|
251
349
|
want_open = OPEN_STATUSES.include?(task['status'])
|
|
252
|
-
|
|
350
|
+
lnk = task.dig('links', 'issue')
|
|
351
|
+
issue = index[id] # already marker-tracked?
|
|
352
|
+
|
|
353
|
+
# Adoption: task points at an existing issue not yet marker-tracked. Adopt it
|
|
354
|
+
# (inject the managed block) instead of creating a duplicate — unless the
|
|
355
|
+
# issue already carries a DIFFERENT marker (foreign provenance → refuse).
|
|
356
|
+
if issue.nil? && lnk
|
|
357
|
+
ls = link_state[lnk.to_i]
|
|
358
|
+
if ls.nil?
|
|
359
|
+
warn "task #{id}: links.issue ##{lnk} not found — creating a new issue instead."
|
|
360
|
+
elsif ls['marker_id'] && ls['marker_id'] != id
|
|
361
|
+
warn "task #{id}: refusing to adopt ##{lnk} — already marked #{ls['marker_id']} (foreign). Skipping."
|
|
362
|
+
next
|
|
363
|
+
else
|
|
364
|
+
issue = ls.merge('adopt' => true)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
253
367
|
|
|
254
368
|
if issue.nil?
|
|
255
369
|
next unless want_open # never create an issue for an already-done task
|
|
256
370
|
|
|
257
|
-
args = ['issue', 'create', '--title', title, '--body',
|
|
371
|
+
args = ['issue', 'create', '--title', title, '--body', render_body(task)]
|
|
258
372
|
managed_labels(task).each { |l| args.push('--label', l) }
|
|
259
373
|
created += 1 if gh.write(args)
|
|
260
374
|
next
|
|
261
375
|
end
|
|
262
376
|
|
|
263
377
|
number = issue['number'].to_s
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
378
|
+
if lnk
|
|
379
|
+
# Human-authored (adopted) issue: NEVER overwrite its title/body — only
|
|
380
|
+
# upsert the managed block (preserving the author's report) + labels.
|
|
381
|
+
gh.write(['issue', 'edit', number, '--body', upsert_managed_block(issue['body'], task)] +
|
|
382
|
+
label_args(managed_labels(task), issue['labels']))
|
|
383
|
+
issue['adopt'] ? (adopted += 1) : (updated += 1)
|
|
384
|
+
else
|
|
385
|
+
# Bot-created issue: fully sync-owned title + body.
|
|
386
|
+
gh.write(['issue', 'edit', number, '--title', title, '--body', render_body(task)] +
|
|
387
|
+
label_args(managed_labels(task), issue['labels']))
|
|
388
|
+
updated += 1
|
|
389
|
+
end
|
|
267
390
|
|
|
268
391
|
if want_open && issue['state'] != 'open'
|
|
269
392
|
gh.write(['issue', 'reopen', number])
|
|
@@ -273,7 +396,7 @@ def sync(data, gh)
|
|
|
273
396
|
end
|
|
274
397
|
end
|
|
275
398
|
|
|
276
|
-
puts "Backlog sync complete: #{created} created, #{updated} updated, #{closed} closed."
|
|
399
|
+
puts "Backlog sync complete: #{created} created, #{adopted} adopted, #{updated} updated, #{closed} closed."
|
|
277
400
|
0
|
|
278
401
|
end
|
|
279
402
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# sync-plan.rb
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Validates (and optionally mirrors) `_data/roadmap_plan.yml` — the ORDER-ONLY
|
|
9
|
+
# plan artifact produced by the /issue-plan committee. The plan sequences open
|
|
10
|
+
# backlog tasks into batches; it must NEVER re-encode anything the backlog owns
|
|
11
|
+
# (risk / priority / area / status / effort), which would create a second source
|
|
12
|
+
# of truth that drifts.
|
|
13
|
+
#
|
|
14
|
+
# ruby scripts/sync-plan.rb --check # validate the plan vs the backlog (CI/PR gate; stdlib only)
|
|
15
|
+
# ruby scripts/sync-plan.rb --dry-run # print the intended pinned-issue upsert
|
|
16
|
+
# ruby scripts/sync-plan.rb # upsert one pinned tracking issue via `gh`
|
|
17
|
+
#
|
|
18
|
+
# --check asserts:
|
|
19
|
+
# * every batch task id exists in _data/backlog.yml and is OPEN (not done),
|
|
20
|
+
# * batch `depends_on` references existing batch ids and the batch DAG is acyclic,
|
|
21
|
+
# * no plan entry carries a backlog-owned field (order-only invariant).
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
require 'yaml'
|
|
25
|
+
require 'date'
|
|
26
|
+
require 'json'
|
|
27
|
+
require 'optparse'
|
|
28
|
+
require 'open3'
|
|
29
|
+
|
|
30
|
+
ROOT = File.expand_path('..', __dir__)
|
|
31
|
+
PLAN_FILE = File.join(ROOT, '_data', 'roadmap_plan.yml')
|
|
32
|
+
BACKLOG = File.join(ROOT, '_data', 'backlog.yml')
|
|
33
|
+
|
|
34
|
+
PIN_MARKER = '<!-- roadmap-plan:pinned -->'
|
|
35
|
+
BACKLOG_OWNED = %w[risk priority area status effort source acceptance title].freeze
|
|
36
|
+
OPEN_TASK_STATUS = %w[open in-progress blocked].freeze
|
|
37
|
+
|
|
38
|
+
def load_yaml(path)
|
|
39
|
+
YAML.load_file(path, permitted_classes: [Date, Time])
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
YAML.safe_load(File.read(path, encoding: 'UTF-8'), permitted_classes: [Date, Time], aliases: false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def backlog_status
|
|
45
|
+
tasks = (load_yaml(BACKLOG)['tasks'] || [])
|
|
46
|
+
tasks.each_with_object({}) { |t, h| h[t['id']] = t['status'] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate(plan, statuses)
|
|
50
|
+
errors = []
|
|
51
|
+
unless plan.is_a?(Hash) && plan['batches'].is_a?(Array)
|
|
52
|
+
return ['plan must have a `batches:` list.']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
batch_ids = plan['batches'].map { |b| b['id'] }.compact
|
|
56
|
+
seen = {}
|
|
57
|
+
batch_ids.each { |bid| seen[bid] ? (errors << "duplicate batch id #{bid}.") : (seen[bid] = true) }
|
|
58
|
+
|
|
59
|
+
plan['batches'].each_with_index do |batch, i|
|
|
60
|
+
where = batch['id'] ? "batch #{batch['id']}" : "batches[#{i}]"
|
|
61
|
+
errors << "#{where}: missing `id`." if batch['id'].to_s.empty?
|
|
62
|
+
|
|
63
|
+
# Order-only invariant: a batch (or its task entries) may not carry a field
|
|
64
|
+
# the backlog owns. Tasks must be bare id strings.
|
|
65
|
+
(BACKLOG_OWNED & batch.keys).each { |k| errors << "#{where}: must not carry backlog-owned field `#{k}`." }
|
|
66
|
+
Array(batch['tasks']).each do |t|
|
|
67
|
+
unless t.is_a?(String) && t =~ /\AT-\d{3,}\z/
|
|
68
|
+
errors << "#{where}: task entries must be bare T-NNN ids (got #{t.inspect})."
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
if !statuses.key?(t)
|
|
72
|
+
errors << "#{where}: task #{t} is not in the backlog."
|
|
73
|
+
elsif !OPEN_TASK_STATUS.include?(statuses[t])
|
|
74
|
+
errors << "#{where}: task #{t} is #{statuses[t]} (only open tasks may be planned)."
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Array(batch['depends_on']).each do |dep|
|
|
79
|
+
errors << "#{where}: depends_on references unknown batch #{dep}." unless seen[dep]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
errors.concat(cycle_errors(plan['batches']))
|
|
84
|
+
errors
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Kahn's algorithm over the batch DAG; any leftover nodes => a cycle.
|
|
88
|
+
def cycle_errors(batches)
|
|
89
|
+
deps = {}
|
|
90
|
+
batches.each { |b| deps[b['id']] = Array(b['depends_on']).dup }
|
|
91
|
+
indeg = Hash.new(0)
|
|
92
|
+
deps.each { |_n, ds| ds.each { |d| indeg[d] += 1 if deps.key?(d) } }
|
|
93
|
+
# Edge dep -> node (dep must precede node); compute order by repeatedly removing
|
|
94
|
+
# nodes whose deps are all satisfied.
|
|
95
|
+
remaining = deps.keys
|
|
96
|
+
progress = true
|
|
97
|
+
while progress
|
|
98
|
+
progress = false
|
|
99
|
+
ready = remaining.select { |n| deps[n].all? { |d| !remaining.include?(d) } }
|
|
100
|
+
unless ready.empty?
|
|
101
|
+
remaining -= ready
|
|
102
|
+
progress = true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
remaining.empty? ? [] : ["batch dependency cycle among: #{remaining.sort.join(', ')}."]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def pinned_body(plan)
|
|
109
|
+
lines = ["#{PIN_MARKER}", '> Auto-managed by `scripts/sync-plan.rb` from `_data/roadmap_plan.yml`.',
|
|
110
|
+
'> Order only — risk/priority/area come from `_data/backlog.yml`.', '']
|
|
111
|
+
plan['batches'].each do |b|
|
|
112
|
+
lines << "### #{b['id']} — #{b['goal']}"
|
|
113
|
+
lines << "Tasks: #{Array(b['tasks']).join(', ')}"
|
|
114
|
+
lines << "Depends on: #{Array(b['depends_on']).join(', ')}" unless Array(b['depends_on']).empty?
|
|
115
|
+
lines << "Test framework: #{b['test_framework']}" if b['test_framework']
|
|
116
|
+
lines << ''
|
|
117
|
+
end
|
|
118
|
+
lines.join("\n").strip
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def upsert_pinned(plan, dry_run:)
|
|
122
|
+
title = '📋 Roadmap plan (auto-managed)'
|
|
123
|
+
body = pinned_body(plan)
|
|
124
|
+
found = `gh issue list --search #{PIN_MARKER.inspect}\\ in:body --state open --json number --jq '.[0].number' 2>/dev/null`.strip
|
|
125
|
+
args =
|
|
126
|
+
if found.empty?
|
|
127
|
+
['issue', 'create', '--title', title, '--body', body, '--label', 'agent-hold']
|
|
128
|
+
else
|
|
129
|
+
['issue', 'edit', found, '--title', title, '--body', body]
|
|
130
|
+
end
|
|
131
|
+
if dry_run
|
|
132
|
+
puts "DRY-RUN gh #{args.join(' ')}"
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
_o, err, st = Open3.capture3('gh', *args)
|
|
136
|
+
warn "gh failed: #{err}" unless st.success?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def main
|
|
140
|
+
mode = :sync
|
|
141
|
+
OptionParser.new do |o|
|
|
142
|
+
o.on('--check') { mode = :check }
|
|
143
|
+
o.on('--dry-run') { mode = :dry_run }
|
|
144
|
+
end.parse!
|
|
145
|
+
|
|
146
|
+
return 0 unless File.exist?(PLAN_FILE) # no plan yet => nothing to validate
|
|
147
|
+
|
|
148
|
+
plan = load_yaml(PLAN_FILE)
|
|
149
|
+
errors = validate(plan, backlog_status)
|
|
150
|
+
unless errors.empty?
|
|
151
|
+
warn '✗ _data/roadmap_plan.yml failed validation:'
|
|
152
|
+
errors.each { |e| warn " - #{e}" }
|
|
153
|
+
return 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if mode == :check
|
|
157
|
+
puts "✓ _data/roadmap_plan.yml is valid (#{plan['batches'].size} batches)."
|
|
158
|
+
return 0
|
|
159
|
+
end
|
|
160
|
+
upsert_pinned(plan, dry_run: mode == :dry_run)
|
|
161
|
+
0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
exit main if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# sync-plan.sh
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# Thin wrapper around scripts/sync-plan.rb.
|
|
7
|
+
#
|
|
8
|
+
# Validates (and optionally mirrors) `_data/roadmap_plan.yml` — the order-only
|
|
9
|
+
# plan artifact produced by the /issue-plan committee — against the backlog.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ./scripts/sync-plan.sh # upsert the pinned tracking issue via gh
|
|
13
|
+
# ./scripts/sync-plan.sh --check # validate plan vs backlog (CI/PR gate)
|
|
14
|
+
# ./scripts/sync-plan.sh --dry-run # print the intended gh call only
|
|
15
|
+
#
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
exec ruby "${SCRIPT_DIR}/sync-plan.rb" "$@"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-zer0
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amr Abdel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -99,6 +99,8 @@ files:
|
|
|
99
99
|
- _data/prerequisites.yml
|
|
100
100
|
- _data/prompts.yml
|
|
101
101
|
- _data/roadmap.yml
|
|
102
|
+
- _data/roadmap_plan.yml
|
|
103
|
+
- _data/routing.yml
|
|
102
104
|
- _data/statistics_config.yml
|
|
103
105
|
- _data/theme-manifest.yml
|
|
104
106
|
- _data/theme_backgrounds.yml
|
|
@@ -491,6 +493,8 @@ files:
|
|
|
491
493
|
- scripts/setup.sh
|
|
492
494
|
- scripts/sync-backlog.rb
|
|
493
495
|
- scripts/sync-backlog.sh
|
|
496
|
+
- scripts/sync-plan.rb
|
|
497
|
+
- scripts/sync-plan.sh
|
|
494
498
|
- scripts/test-auto-version.sh
|
|
495
499
|
- scripts/test-mermaid.sh
|
|
496
500
|
- scripts/test-notebook-conversion.sh
|