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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2871e1d0cd0522e37b6bac85065edfe6767fc749c39a0ac257fec51dbff97691
4
- data.tar.gz: 12d1d870ed1115cd2044446c66b318f4d6e6c65b1ca3a59493353e5a9d6402a9
3
+ metadata.gz: 8552bc1992ed59b6f63392207f6a49d12bc9e66ac16cd43b61bb57700f45395e
4
+ data.tar.gz: 54304e964cff2dc7fafe3ab03c722dfac994011994be96201d700a67e224ec35
5
5
  SHA512:
6
- metadata.gz: 25198dd08404a6ae41724e8e172ce38f832f19b34efd83761cff86ba033c5952e2d315dad70c75b343cb0c94e216c1d5755a0c53048252a2bd7a1c1a61ac590b
7
- data.tar.gz: 49d28d829375c95f39986c6e19ff37d524e06f00ae7c388b743e6cd073a35f9c4717aa47f877bb8802d7c5f12fc1f2dd2d618c12785a6342f8a2164774fe6576
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 }
@@ -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
- # Map of backlog id -> existing issue {number, state, labels} via the body marker.
206
- def existing_issues(gh)
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
- index = {}
213
- JSON.parse(raw).each do |issue|
214
- next unless issue['body'] =~ /<!-- backlog-id: (T-\d+) -->/
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
- index[Regexp.last_match(1)] = {
217
- 'number' => issue['number'],
218
- 'state' => issue['state'].to_s.downcase,
219
- 'labels' => (issue['labels'] || []).map { |l| l['name'] }
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
- index
223
- rescue JSON::ParserError
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
- index = existing_issues(gh)
245
- created = updated = closed = 0
246
-
247
- (data['tasks'] || []).each do |task|
248
- id = task['id']
249
- title = task['title']
250
- body = render_body(task)
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
- issue = index[id]
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', 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
- gh.write(['issue', 'edit', number, '--title', title, '--body', body] +
265
- label_args(managed_labels(task), issue['labels']))
266
- updated += 1
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.20.2
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-25 00:00:00.000000000 Z
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