Repository navigation
Build out the template: scripts, tests, Makefile, CI, flake, skills - #1
Conversation
…3 fixes This repository is now the upstream of the shared functional-Python core; the three consumer projects re-vendor from here. The copies were identical across agda-native-air, agda-algebras, and williamdemeo.github.io as of 2026-08-13; this vendoring applies the five utils fixes from ualib/agda-algebras#293, each pinned by a regression test that fails against the unfixed copies (verified: 8 failures/errors before the fixes, 21/21 green after): 1. Result.unwrap() keys solely off _is_ok, so Ok(None) — routine, e.g. from write_text — unwraps to None instead of raising. unwrap_or() had the same defect class and is fixed alongside. 2. Result.map() lets exceptions from the mapped function propagate instead of synthesizing Result.err(Exception) in a Result[_, PipelineError]. 3. file_ops imports ProcessingStage, which calculate_file_metadata annotates; typing.get_type_hints raised NameError before. 4. command_runner stream_output=True drains stdout and stderr in concurrent threads; draining stdout to completion first deadlocked (deterministically, ~64KiB stderr) — reproduced before the fix. 5. stdout_file opens in a mode matching text=. (The TypeError claimed in #293 did not reproduce on Python 3.12 — the child writes to the descriptor directly — so the test pins the now-explicit contract.) text_processing is deliberately not vendored: it is doc-pipeline specific and the gh_project scripts do not use it; it remains owned by the downstream projects. AI-assisted: Claude Fable 5 (Anthropic)
…lient Ported from williamdemeo.github.io's copy (the newest lineage; verified by diff to strictly subsume agda-algebras'), restructured as functional-core / imperative-shell: - GitHubClient.snapshot() fetches labels, milestones, and issues once per run (three calls total); the write side is now plain creations with no hidden refetch. This is the structural fix for #293 item 6 (idempotency checking was O(n^2): one ListIssues per create call). - plan_labels/plan_milestones/plan_issues are pure snapshot x plan reconcilers. plan_labels never overwrites and reports near-collisions (case/whitespace, e.g. 'era: conway' vs 'era:conway') instead of creating a parallel scheme. - plan-file parsing moves here from populate so lint can share it. Issue bodies now end at the next issue heading, top-level section, or END GENERATED marker (the ancestors stopped only at '## Summary:', sweeping trailing prose into the final issue's body). - Issue headings may carry a recorded '(#N)' suffix (populate's crash-safe write-back); it parses as state, not title. record_issue_number() is the targeted, line-oriented writer. - parse_file() additionally rejects stray END and nested BEGIN markers. - Issue records carry assignees (GitHub-owned state the update script will render). Also: close the drained pipes in command_runner's streaming path and silence the drain logger inside the flood test. AI-assisted: Claude Fable 5 (Anthropic)
gh_project_populate.py (file -> GitHub), ported from the newest lineage and extended per the design requirements: - one RepoSnapshot per run feeds the pure planners (#293 item 6 closed; regression: the fake-gh transcript shows exactly one list call per entity kind however many issues the plan holds) - crash-safe write-back: each created issue's number is recorded as a (#N) heading suffix and persisted before the next remote mutation (regression: a run that dies at issue 3 of 4 reruns to exactly 4 issues, zero duplicates) - label reconciliation: near-collisions (case/whitespace) are reported and skipped; existing labels are never overwritten (both pinned by integration tests) - --repo falls back to the plan file's **Repository**: header; --yes for non-interactive runs; --delay default 1.0s; --start-from now uses the documented >= ordering (the ancestor's loop used equality, silently skipping everything when the ID never matched) - refuses to run against a plan with lint errors gh_project_update.py (GitHub -> file) is the renamed render — 'update' names the intent (bring the file up to date), not the direction — with diff(1) exit codes kept (0 current / 1 drift / 2 failure) and assignees now rendered (GitHub-owned state the ancestor omitted). gh_project_lint.py is the no-network structural gate (markers balanced, unique, un-nested; labels section parses with no case/whitespace collisions; milestone references consistent; issue IDs unique; label references resolvable). Populate shares the same lint_plan core. scripts/tests/fake_gh/ is a recorded-transcript gh stand-in: the integration tests run the real scripts as subprocesses with it on PATH, so command construction, JSON parsing, write-back, and exit codes are exercised with zero network. AI-assisted: Claude Fable 5 (Anthropic)
The Makefile is the primary UX: make help (default), populate-dry, populate, update, update-check, lint, test. REPO defaults to the plan file's **Repository**: header; NO_ENV_PREFIX passes through for environments that authenticate via GH_TOKEN/GITHUB_TOKEN. docs/GITHUB_PROJECT.md is a deliberately small worked example (2 milestones, 4 issues) that a consumer replaces wholesale; every construct the parsers understand appears exactly once, including the load-bearing milestone-N-* labels, the hand-authored mermaid dependency graph, and the generated-region markers. It lints clean. scripts/VERSION (0.1.0) is the vendoring marker consumers use to tell which release of the scripts directory they carry. AI-assisted: Claude Fable 5 (Anthropic)
ci.yml: make lint + make test on Python 3.11 and 3.12 (setup-python, deliberately not Nix — the flake is a convenience, never a requirement), shellcheck over scripts/ci/, and ruff (E9,F only) over scripts/. project-plan-update.yml runs Mondays and on workflow_dispatch. All logic lives in scripts/ci/project-plan-update.sh (runnable, linted, tested locally; the YAML holds only schedule, permissions, and the call). Shipped default is REPORT-ONLY: drift lands in the step summary and the job always succeeds — no bot commits. Setting the repository variable PROJECT_PLAN_AUTO_UPDATE to 'pr' or 'commit' arms the automatic update. The script keeps gh_project_update.py's diff(1) exit codes distinct so 'the plan drifted' is never conflated with 'the check could not run' (all three paths exercised locally via the UPDATE override). Also: drop unused imports and a placeholder-less f-string that the vendored pipeline_types carried from its ancestors (ruff E9,F now gates them). AI-assisted: Claude Fable 5 (Anthropic)
flake.nix: a devShell (python3, gh, gnumake) so a machine with only Nix can run the tooling. Strictly optional — CI uses setup-python and the README documents the plain path first. Verified: nix develop provides python3/gh/make. .claude/skills/: committed config as product, for template consumers. project-setup covers the bootstrap (author plan -> lint -> dry-run -> populate -> update); roadmap-upkeep covers steady state (update/ update-check, adding issues/milestones to a live project, arming the freshness workflow). CLAUDE.md states the structure/state contract and its editing corollaries. README: quickstart from 'Use this template' through the steady-state loop, the opt-in freshness story (PROJECT_PLAN_AUTO_UPDATE=pr|commit), the one-liner re-vendor upgrade path pinned to scripts/VERSION, the Nix path, layout, and design notes. AI-assisted: Claude Fable 5 (Anthropic)
Caught during the live E2E: GNU make maps any failing recipe to its own exit 2, so 'make update-check' cannot relay the update script's diff(1)-style exit 1 for drift. The script's 0/1/2 contract is real (verified live: drift exits 1, make wraps it to 2) and the freshness workflow's shell calls the script directly, so its drift-vs-failure branching is unaffected; only the README and the roadmap-upkeep skill promised codes the make path cannot deliver. Both now state: the make target succeeds/fails with explanatory output; call the script directly when the exit code must distinguish drift from a broken check. AI-assisted: Claude Fable 5 (Anthropic)
Quality-gate evidence (all four gates met)#293 fixes — all six items fixed, each with a regression test; the suite was run against the pristine (unfixed) vendored copies first: 8 failures/errors, then 86/86 green after the fixes. Item 6 (O(n²) idempotency) is pinned by the fake-gh transcript asserting exactly one list call per entity kind per run. Live E2E (throwaway README commands — every command executed as written from a fresh clone via BOTH paths (plain: system python3/gh/make; Nix: Scheduled workflow — tested twice via One fix fell out of the E2E: GNU make maps any failing recipe to exit 2, so Cleanup (needs the gh auth refresh -h github.com -s delete_repo
gh repo delete williamdemeo/gh-project-template-e2e --yes |
There was a problem hiding this comment.
Pull request overview
Builds the repository into a functional GitHub roadmap template and addresses the six issues tracked in agda-algebras#293.
Changes:
- Adds roadmap parsing, linting, population, synchronization, and CI automation.
- Vendors corrected functional utilities with an 86-test offline suite.
- Adds documentation, example plans, Nix support, and workflow skills.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
scripts/VERSION |
Sets the tooling version. |
scripts/tests/test_pipeline_types.py |
Tests Result regressions. |
scripts/tests/test_integration.py |
Tests end-to-end roadmap workflows. |
scripts/tests/test_gh_project_update.py |
Tests update rendering. |
scripts/tests/test_gh_project_lint.py |
Tests structural linting. |
scripts/tests/test_gh_project_lib.py |
Tests parsing and planning. |
scripts/tests/test_file_ops.py |
Tests filesystem utilities. |
scripts/tests/test_command_runner.py |
Tests command execution regressions. |
scripts/tests/fake_gh/gh |
Provides an offline GitHub CLI fake. |
scripts/gh_project_update.py |
Synchronizes GitHub state into Markdown. |
scripts/gh_project_populate.py |
Creates GitHub roadmap entities. |
scripts/gh_project_lint.py |
Validates roadmap structure. |
scripts/ci/project-plan-update.sh |
Implements freshness automation. |
scripts/_utils/pipeline_types.py |
Provides functional result types. |
scripts/_utils/file_ops.py |
Provides filesystem wrappers. |
scripts/_utils/command_runner.py |
Provides subprocess execution. |
scripts/_utils/__init__.py |
Exports utility APIs. |
scripts/_gh_project_lib.py |
Implements the shared roadmap core. |
README.md |
Documents setup and operation. |
Makefile |
Adds primary user commands. |
flake.nix |
Defines an optional development shell. |
flake.lock |
Pins Nix dependencies. |
docs/GITHUB_PROJECT.md |
Supplies the worked roadmap example. |
CLAUDE.md |
Defines repository workflow conventions. |
.github/workflows/project-plan-update.yml |
Adds scheduled freshness checks. |
.github/workflows/ci.yml |
Adds tests and static checks. |
.claude/skills/roadmap-upkeep/SKILL.md |
Documents roadmap maintenance. |
.claude/skills/project-setup/SKILL.md |
Documents project bootstrap. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| parsed = parse_issue_id(title) | ||
| if parsed is None: | ||
| # Not a planning issue (e.g. an ad-hoc bug report); skip | ||
| # silently — update does not list these. | ||
| continue |
There was a problem hiding this comment.
Confirmed — this was the sharpest finding of the review. The fallback was dead code: _parse_issues_json dropped prefix-less issues before plan_issues ever saw them, and the unit test had simulated the wrong scenario (a changed ID, which still parses) so it passed anyway. Fixed in 9ad5f20: the parser now keeps unparseable-title issues with id="" (and filters PRs, which the REST endpoint includes); populate matches them by recorded number; update's group_by_milestone excludes them from rendering. Covered by a corrected unit test, a by_id-guard test (blank ids never match each other), and a new integration test that strips a live title's prefix and re-runs populate with zero duplicates.
| if write_result.is_err: | ||
| print( | ||
| f" ~ write-back failed: {write_result.unwrap_err()}", | ||
| file=sys.stderr, | ||
| ) |
There was a problem hiding this comment.
Agreed — continuing past a failed write-back violated the persist-before-next-mutation contract stated in the same docstring. Fixed in 9ad5f20: either failure mode (heading not found, or the write itself failing) increments failed, prints which issue IS live on GitHub, and aborts before any further remote mutation; the rerun is safe via the snapshot match. New integration test: a chmod-444 plan file aborts after the first creation (1 issue live, exit 1), and the rerun completes to exactly 4 with no duplicates.
| problems.append(Problem( | ||
| "warning", f"milestone number {n} is defined more than once")) |
There was a problem hiding this comment.
Agreed — ms_title_map keys on the plan number, so a duplicate makes issue→milestone attachment ambiguous (last definition wins, silently). Escalated from warning to error in 9ad5f20; populate's lint gate now refuses to run. The lint test was flipped to expect the error.
| problems.append(Problem( | ||
| "warning", f"milestone number {n} is defined more than once")) | ||
|
|
||
| issue_ids = [i.id for i in plan.issues] |
There was a problem hiding this comment.
Agreed, and the failure mode is nasty: a typo'd heading inside a generated region isn't just skipped — the next make update erases it when the region is rebuilt from GitHub. Fixed in 9ad5f20: lint scans every ### Issue-prefixed line and errors on any that ISSUE_HEADING_RE doesn't match, with the line number and an explanation of the erasure hazard. Test: ### Issue M1-X: ... is now a lint error.
| start = section.end() | ||
| nxt = re.search(r"^## (?!#)", text[start:], re.MULTILINE) | ||
| end = start + nxt.start() if nxt else len(text) | ||
| entry_starts = re.finditer(r"^\s*[-*]\s+`", text[start:end], re.MULTILINE) |
There was a problem hiding this comment.
Agreed — the old count only saw bullets that already opened a backtick, so - documentation (0e8a16) — ... was invisible; in the worst case (all entries backtick-less) the explicit section parsed as empty and lint skipped the reference check entirely. Fixed in 9ad5f20: every bullet line in the ## Labels section must now match the full - \name` (RRGGBB) — description` shape, each failure reported with its own line number. Test added.
| "issue", "list", | ||
| "--repo", self.repo, | ||
| "--state", "all", | ||
| "--limit", "1000", |
There was a problem hiding this comment.
Agreed — a silently truncated snapshot would defeat exactly the guarantees it feeds (idempotency for populate, completeness for update). Fixed in 9ad5f20: list_issues moves to gh api repos/{repo}/issues --paginate (with a pull_request-key filter, since the REST endpoint includes PRs), and list_labels — which had the same defect at --limit 200 — moves to repos/{repo}/labels --paginate. The fake-gh transcript was updated to the REST invocations, and both paths were verified against live GitHub, including PR filtering.
| if ms_title is None and issue.milestone_idx != 0: | ||
| print( | ||
| f" ~ issue {issue.id}: milestone {issue.milestone_idx} " | ||
| f"unavailable; creating without a milestone" | ||
| ) |
There was a problem hiding this comment.
Agreed — and the incompleteness was permanent, not just transient: populate never revisits existing issues, so a milestone-less creation would never be healed. Fixed in 9ad5f20: when the issue's milestone is declared in the plan but unavailable on GitHub, the issue is skipped and counted as a failure (exit 1) with a create-milestones-first hint. Plans that declare no milestones at all still populate milestone-free, which keeps the labels+issues-only use case working. Integration test: --issues-only on a fresh repo exits 1 and creates nothing.
Every finding was real; all seven are fixed, with tests: 1. The recorded-(#N) fallback in plan_issues was unreachable: the snapshot parser dropped any live issue whose [MN-k] prefix had been stripped, so the planner never saw its number and would re-create it. _parse_issues_json now keeps such issues with id=""; populate matches them by number, update filters them out of rendering. The original unit test had simulated the wrong scenario (a changed ID, which still parses) — replaced with the real one, plus an integration test: strip a live title's prefix, re-run populate, zero duplicates. 2. A failed issue-number write-back now aborts the run (counted as a failure) instead of continuing to mutate GitHub while numbers stop landing on disk — that continuation violated the stated persist-before-next-mutation contract. Integration test: chmod-444 plan file aborts after the first creation; rerun completes to four issues with no duplicates. 3. Duplicate milestone numbers escalate from lint warning to error: ms_title_map keys on the number, so a duplicate makes issue->milestone attachment ambiguous. 4. Lint now rejects any '### Issue ...' heading that does not match 'Issue MN-k: Title' — a malformed heading (e.g. M1-X) was invisible to populate and would be silently erased when update rebuilt its region. 5. The ## Labels well-formedness check now validates every bullet in the section, not just bullets that already open a backtick — a backtick-less entry was invisible to the old count. 6. list_issues and list_labels move from fixed --limit ceilings (1000 / 200) to REST endpoints with --paginate; the issues parser filters pull requests (the REST endpoint includes them). Verified against live GitHub, including PR filtering. 7. An issue whose plan-declared milestone is unavailable is skipped and counted as a failure instead of being created milestone-less — populate never revisits existing issues, so that incompleteness was permanent and silent. Plans that declare no milestones still populate milestone-free. Integration test: --issues-only on a fresh repo exits 1 and creates nothing. Suite grows 86 -> 95 tests, all green; REST switch live-verified against the E2E throwaway. AI-assisted: Claude Fable 5 (Anthropic)
Every finding was real; all seven are fixed, with tests: 1. The recorded-(#N) fallback in plan_issues was unreachable: the snapshot parser dropped any live issue whose [MN-k] prefix had been stripped, so the planner never saw its number and would re-create it. _parse_issues_json now keeps such issues with id=""; populate matches them by number, update filters them out of rendering. The original unit test had simulated the wrong scenario (a changed ID, which still parses) — replaced with the real one, plus an integration test: strip a live title's prefix, re-run populate, zero duplicates. 2. A failed issue-number write-back now aborts the run (counted as a failure) instead of continuing to mutate GitHub while numbers stop landing on disk — that continuation violated the stated persist-before-next-mutation contract. Integration test: chmod-444 plan file aborts after the first creation; rerun completes to four issues with no duplicates. 3. Duplicate milestone numbers escalate from lint warning to error: ms_title_map keys on the number, so a duplicate makes issue->milestone attachment ambiguous. 4. Lint now rejects any '### Issue ...' heading that does not match 'Issue MN-k: Title' — a malformed heading (e.g. M1-X) was invisible to populate and would be silently erased when update rebuilt its region. 5. The ## Labels well-formedness check now validates every bullet in the section, not just bullets that already open a backtick — a backtick-less entry was invisible to the old count. 6. list_issues and list_labels move from fixed --limit ceilings (1000 / 200) to REST endpoints with --paginate; the issues parser filters pull requests (the REST endpoint includes them). Verified against live GitHub, including PR filtering. 7. An issue whose plan-declared milestone is unavailable is skipped and counted as a failure instead of being created milestone-less — populate never revisits existing issues, so that incompleteness was permanent and silent. Plans that declare no milestones still populate milestone-free. Integration test: --issues-only on a fresh repo exits 1 and creates nothing. Suite grows 86 -> 95 tests, all green; REST switch live-verified against the E2E throwaway. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated 1 comment.
Suppressed comments (10)
scripts/gh_project_update.py:132
- The renderer emits
**Labels**:, while_parse_issuesand the documented plan format recognize only**Labels:**. Aftermake update, reparsing treats this metadata as body text and yields an empty label tuple; a new issue copied from the rendered format would be created without labels. Render the canonical**Labels:**form and update the corresponding assertion.
f"**Labels**: {labels}\n"
scripts/tests/test_gh_project_update.py:32
- This assertion enshrines the non-canonical
**Labels**:spelling even though the plan parser accepts**Labels:**. Update it with the renderer and add a round-trip assertion that reparsed issues retain their labels.
self.assertIn("**Labels**: `milestone-1-core`\n", out)
scripts/gh_project_populate.py:382
- Dry-run exits based only on label collisions, but live-only prerequisite checks happen later in
execute_issues. For example,--dry-run --issues-onlyon a repository with no milestones reports all issues as creatable and exits 0, while the real run skips every issue and exits 1. Move milestone/label availability into the pure issue plan so dry-run reports and returns exactly what execution will do.
if args.dry_run:
print("Dry run — nothing was created.")
return 1 if collisions else 0
scripts/_gh_project_lib.py:705
- This guard disables milestone-reference validation when a
## Milestonessection exists but none of its headings parse. A one-milestone plan using-instead of—, for example, produces an empty tuple, passes lint, and then creates all issues without a milestone. Guard on the section's presence so malformed headings cannot bypass the consistency checks.
if plan.milestones:
scripts/_utils/pipeline_types.py:262
Path.suffixreturns only.mdforModule.lagda.md, so this property returns false for a file type explicitly listed as supported. Match against the filename (orsuffixes) so literate Markdown Agda sources are recognized.
return self.source_path.suffix in {'.agda', '.lagda', '.lagda.md'}
scripts/_gh_project_lib.py:178
- Treating every level-2 heading as the end of an issue silently truncates valid Markdown bodies. An issue containing
## Acceptance criteriais created with everything from that heading onward omitted. Bound bodies by the next issue heading or generated-region end instead of generic Markdown headings.
BODY_END_RE = re.compile(r"^(?:## (?!#)|<!--\s*END GENERATED)", re.MULTILINE)
scripts/gh_project_populate.py:287
- These stage-selection flags are not mutually exclusive, and any pair makes all three
do_*values false, so the command fetches a snapshot, reports success, and performs no work. Define them in an argparse mutually exclusive group so contradictory invocations fail immediately.
parser.add_argument("--milestones-only", action="store_true")
parser.add_argument("--labels-only", action="store_true")
parser.add_argument("--issues-only", action="store_true")
scripts/gh_project_populate.py:438
- This created count is wrong when write-back aborts the loop. If the first of four issues is created but cannot be persisted,
issue_failedis 1 and the summary claims three issues were created even though the remaining three were not attempted. Haveexecute_issuesreturn an explicit created/attempted count and report that value.
f"DONE: {len(issue_plan.to_create) - issue_failed} issues created, "
f"{len(issue_plan.existing)} already existed, {issue_failed} failed"
scripts/gh_project_update.py:128
- Live issue bodies are inserted without neutralizing the file's control markers. If a GitHub body contains
<!-- END GENERATED: milestone-N -->, the next parse can close the region inside that body and treat the remaining generated content as manual prose, causing stale/duplicated content to survive updates. Escape delimiter sequences in dynamic titles/bodies or use framing that cannot occur in issue content.
body = issue.body.strip() if issue.body else "_(no description on GitHub)_"
scripts/_gh_project_lib.py:456
- A stray END marker is checked only when there is no later BEGIN. Thus
<!-- END GENERATED: stray -->before any valid region is silently absorbed into a manual segment, despite lint claiming to reject stray markers. At each position, compare the next BEGIN and END and error when END occurs first.
begin = BEGIN_RE.search(content, pos)
if begin is None:
# A stray END with no matching BEGIN would otherwise vanish
# silently into a manual segment.
stray = END_RE.search(content, pos)
| def _parse_json(stdout: str, kind: str) -> Result[list[dict], PipelineError]: | ||
| """Decode the JSON body of a `gh` invocation, with structured errors.""" | ||
| try: | ||
| data = json.loads(stdout) |
There was a problem hiding this comment.
Confirmed — a real bug I introduced with the REST switch, and the ancestors' paginated milestones call carried the same latent defect. Fixed in 634ca0e, with one deviation from the suggested remedy: instead of --slurp (which requires gh ≥ 2.47 and would silently raise the template's documented "any gh" floor — the machine this was built on runs 2.45), _parse_json now decodes the concatenated output document-by-document with json.JSONDecoder.raw_decode and concatenates the lists. Same behavior for every gh version, single-document output unaffected, and all three list calls (labels, milestones, issues) go through the one decoder. Pinned by six new unit tests (multi-document, newline-separated, single, empty, non-list, trailing garbage) and live-verified against a real repository.
…ndings
The explicit comment was a real bug introduced by the REST switch:
gh api --paginate concatenates pages as separate JSON documents, so a
single json.loads fails with 'Extra data' past 100 items. _parse_json
now decodes document-by-document with raw_decode and concatenates —
chosen over gh's --slurp, which requires gh >= 2.47 and would raise the
documented 'any gh' floor. (The ancestors' paginated milestones call
carried the same latent bug.)
All ten suppressed comments were also legitimate; each is fixed:
- Render/parse asymmetry: update now emits canonical **Labels:** and
**Assignees:** (the spelling the plan parser recognizes), populate
strips Assignees metadata from bodies, and a round-trip test pins
that a rendered block reparses with labels intact. Consequence
handled: the label-reference lint no longer applies to issues with a
recorded (#N) — their labels line is rendered state and may carry
GitHub-added labels.
- BODY_END_RE had regressed the exemplar format: real plans put
'## Tasks' / '## Acceptance criteria' sections INSIDE issue bodies.
Bodies are now bounded by region markers and the two structural
headers the ancestors special-cased ('## Milestone N', '## Summary'),
not by generic '## ' headings; the fixture now includes a ##-section
body.
- Dry run now predicts execution exactly: the shared issue_blocker
computes label/milestone availability from the mode-appropriate
prospective sets, prints '! blocked' lines, and drives the exit code.
- Milestone-consistency lint guards on the section's PRESENCE, and
numbered-but-malformed '### Milestone 1 - Core' headings (hyphen for
em-dash) are their own error — an all-malformed section can no longer
bypass validation. Prose headings like '### Milestone dependencies'
(used by the exemplars) stay legal.
- Live bodies containing region-marker text are defanged on render
('END GENERATED (escaped):'), so a hostile or accidental marker in a
GitHub issue body cannot corrupt the next parse.
- parse_file catches a stray END even when a valid region follows it.
- The three --*-only flags are mutually exclusive (any pair silently
deselected every stage).
- execute_issues returns explicit created/failed counts; the DONE
summary no longer claims 'planned - failed' were created after an
abort, and reports the not-attempted remainder.
- Vendored pipeline_types.is_agda_file matches the filename, not
Path.suffix, so 'Module.lagda.md' is recognized (this repo is the
utils upstream; downstreams inherit the fix on re-vendor).
Suite grows 95 -> 111, all green; canonical rendering and pagination
verified live against the E2E throwaway.
AI-assisted: Claude Fable 5 (Anthropic)
9ad5f20 to
634ca0e
Compare
Response to the ten suppressed comments (review of 634ca0e)Copilot's confidence filter was too conservative: all ten suppressed findings were legitimate, and all ten are fixed in 634ca0e. The test suite grew 95 → 111. Item by item, in the review's order:
Everything re-verified live against the E2E throwaway: canonical rendering round-trips ( |
All three findings were legitimate documentation inaccuracies: - --issues-only requires referenced LABELS to exist, not only milestones — the mode creates neither, and issue_blocker counts an issue with an unavailable label as a failure. Fixed in HowToRun and in populate's own module docstring, which carried the same milestone-only claim since PR #1. - Exit status 1's unqualified 'were skipped' invited misreading: --start-from filtering and already-existing items exit 0; only blocked issues (unavailable label/milestone), creation errors, collisions, and sync refusals reach exit 1. The wording now says exactly that, and exit 0 explicitly covers deliberate filtering. - 'List items survive byte-for-byte' overclaimed: list STRUCTURE is preserved (markers, one item per line, empty items untouched) while wrapped prose INSIDE an item is deliberately reflowed — half the feature. The unwrap section now draws that line. AI-assisted: Claude Fable 5 (Anthropic)
Builds the empty stub into the working template.
What's here
_utils(pipeline_types, file_ops, command_runner) with all five utils fixes from Follow-up to #289 — known issues in the gh_project tooling ualib/agda-algebras#293, each pinned by a regression test that fails against the unfixed copies (verified: 8 failures/errors before, green after). This repo is now the upstream of the shared functional core.scripts/_gh_project_lib.py— shared model + parsing + pure planners + a snapshot-basedghclient. Populate now fetches live state once per run (#293 item 6, the O(n²) idempotency check) and plans against it.gh_project_populate.py(file → GitHub, crash-safe(#N)write-back after each creation, label near-collision reconciliation),gh_project_update.py(GitHub → file; the renamed render, diff(1) exit codes, assignees rendered),gh_project_lint.py(structural gate, no network).ghon PATH — one-snapshot-per-run, rerun-after-crash-without-duplicates, collision-skip, and update round-trip are all pinned.helpdefault;populate-dry/populate/update/update-check/lint/test).docs/GITHUB_PROJECT.md(2 milestones, 4 issues) that lints clean and demonstrates every construct once.PROJECT_PLAN_AUTO_UPDATE=pr|commitarms automatic updates. All logic in a locally-testable shell script (all three advisory paths exercised).CLAUDE.md(the structure/state contract) and two skills (project-setup,roadmap-upkeep).scripts/VERSION.Follow-up (separate PRs, William-gated)
Adoption PRs re-vendoring
scripts/into agda-algebras, williamdemeo.github.io, and agda-native-air; close ualib/agda-algebras#293 from the aa PR.AI-assisted development: Claude Fable 5 (Anthropic)