CR-010.R6: scheduler-grade pipeline contract (--headless / --json / exit codes) - #244
Merged
Merged
Conversation
`graq rebuild` returns an int that Typer discards, so every invocation exits 0
— including "Graph file not found". Measured: a missing graph prints an error
in red and exits 0, so an unattended scheduler cannot detect the failure at
all. R6's acceptance criterion is that a failed step be distinguishable from an
empty delta by exit code alone; today they are both 0.
Adds graqle/cli/headless.py as the one place the machine contract is defined:
0 success, 1 failure, 2 usage, 3 empty delta. EMPTY_DELTA is deliberately
distinct from SUCCESS so a scheduler can also gate downstream work on whether
anything actually changed. RunReport is frozen and derives exit_code from
status, so a serialized report can never carry a pair that disagree; errors
carry exception TYPE names only, inheriting the PII rule already enforced on
.graqle/govern.health.json.
--headless and --json stay orthogonal: --headless is about interactivity,
--json about format. Coupling them would contradict the standalone --json
convention already used at 15+ sites and hand JSON to scripts that never asked
for it.
Incremental rebuild becomes change-based. rebuild_chunks previously skipped any
node that already had chunks, so a node whose source was edited kept stale
evidence — a correctness gap, not just a perf one. --incremental compares a
stored SHA-256 of the source content. Content, not mtime: git checkouts, CI
clones and Docker layer caching all rewrite mtime, which is exactly where a
scheduler runs. Graphs with no stored hash fall back to the old behaviour.
Backwards compatibility: the corrected exit codes apply only when a machine
flag is passed. Bare `graq rebuild` behaves byte-for-byte as before and gets a
stderr DeprecationWarning naming the future change. `govern serve --once`
without --json is untouched.
Sentinel (graq_reason, 2 passes; graq_review is down). Pass 1 raised 4
BLOCKERs; each was reproduced before being fixed:
- BLOCKER-1 REAL: a run where every node failed reported empty_delta/exit 3
with errors:[] — "healthy, nothing to do" while nothing worked. Fixed:
rebuild_chunks counts failures into Graqle.last_rebuild_failed_nodes and
any failure now yields FAILURE.
- BLOCKER-2 REFUTED: the two property writes are adjacent in-memory dict
assignments with no I/O between them, and the worst case (hash missing)
re-chunks next run rather than skipping stale evidence.
- BLOCKER-3 REFUTED by measurement: bare rebuild updating 3 nodes exits 0,
not 3 — Typer 0.24.1 discards the return value. That IS the defect here.
- BLOCKER-4 REFUTED by measurement on win32: NamedTemporaryFile is used as a
context manager, so the handle closes before os.replace. Report written,
zero orphan tempfiles.
Pass 2 found a further real BLOCKER: partial failure (some nodes rebuilt, some
failed) fell into the "updated" branch and exited 0. A scheduler routes on the
exit code, so that is the same silent success in a smaller costume — any
failure is now a FAILURE.
graq_predict then surfaced a defect neither pass caught: rebuild_command is
also called programmatically by `graq init`, where unfilled typer.Option
defaults arrive as truthy OptionInfo sentinels. Those wrongly selected machine
mode and then raised TypeError on Path(OptionInfo), which would have broken
init's auto-rebuild. Flags are now normalised before use. Verified the direct
call returns a plain int again.
Also ships cron / Airflow / GitHub Actions reference recipes, as the
requirement asks.
Tests: 913 passed across tests/test_core + tests/test_cli. The 10 failures in
test_gate_install.py / test_g5_vscode_gate_install.py are pre-existing —
verified by stashing this branch and reproducing them on untouched
private/master; they reference none of these modules.
TS-screen: 0 hits.
Plan: plan_b48cef6f
CR: .gsm/external/Change Requests/CR-010.R6-scheduler-pipeline-contract.md
(cherry picked from commit 95813097d9a15fbcf538a8881df6e40af4d13268)
🛡️ GraQle PR Guardian💥 Blast Radius: 7 modules affected
Total blast radius: 7 🏛️ Governance Verdict🚫 FAIL
🔍 SHACL ViolationsNo SHACL violations detected. ✅ 🔐 Approval RequirementsThis PR requires approval from:
❌ Approval requirement NOT yet satisfied.
🔬 Powered by GraQle PR Guardian v0.1.0 · Scan completed 2026-07-29T20:18:28.085511+00:00 |
harishquantamix
approved these changes
Jul 29, 2026
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CR-010.R6 — Scheduler-grade pipeline contract
Public cherry-pick of private PR #322 (merged
0d6bda41, feature commit95813097). Cherry-picked aseedc4175onto the currentorigin/master(c51d1f26), with the suite and trade-secret screen re-run on that base.Closes leg 4 of the CR-010 enterprise adoption test: pip install → config file → secret path → scheduler entry. Legs 1–3 already hold via R4 + R5 (both live in 0.81.0).
The defect
graq rebuildis annotated-> intand returns a node count, butmain.py:122registers it directly with Typer — which discards return values. Measured on typer 0.24.1: a command returning7exits 0.Separately,
rebuild_chunks(force=False)skipped any node that already had chunks, so a node whose source file was edited kept stale evidence — a correctness gap, not just a performance one.The contract
0123EMPTY_DELTAis deliberately distinct fromSUCCESS: collapsing both onto 0 leaves "did work actually happen?" unanswerable without parsing stdout, so a scheduler could not gate a downstream deploy on whether the graph changed.New
graqle/cli/headless.py:RunReportis frozen, andexit_codeis a derived property — a serialized report can never carry a status/exit_code pair that disagree.errorscarries exception type names only, inheriting the PII rule already enforced on.graqle/govern.health.json(reports get archived as scheduler artifacts).--report-jsonreuses the existing atomic tempfile +os.replacepattern fromgovern_serve.py, including orphan-tempfile cleanup.--headlessand--jsonstay orthogonal — interactivity vs format. Coupling them would contradict the standalone--jsonconvention at 15+ existing sites.--incrementalis content-hash based (SHA-256 in_chunk_content_hash), not mtime: git checkouts, CI clones and Docker layer caching all rewrite mtime, which is exactly where a scheduler runs. Graphs with no stored hash fall back to the previous behaviour.Also ships cron / Airflow / GitHub Actions reference recipes (
docs/scheduler-recipes.md).Backwards compatibility
The corrected exit codes apply only when
--headless,--json, or--report-jsonis passed — those callers are new by definition. Baregraq rebuildbehaves byte-for-byte as before, with a stderr-onlyDeprecationWarningnaming the future change.govern serve --oncewithout--jsonis untouched (38 existing tests unchanged).A follow-up release may make bare invocation adopt the corrected exit code as an explicitly-flagged breaking change. Not in this PR.
Verified on this base
--headless --jsonexit 1,errors:["GraphFileNotFound"]--incrementalexit 3empty_delta--incrementalexit 0,updated=1← change-basedexit 3graq rebuild, missing graphexit 0← legacy contract preserved914 tests pass across
tests/test_core/+tests/test_cli/. The 10 failures intest_gate_install.py/test_g5_vscode_gate_install.pyare pre-existing — verified by checking outc51d1f26(this PR's base, without the commit) and reproducing the identical 10. They reference none of these modules.Trade-secret screen: 0 hits.
Verified that the R6 changes are byte-identical to the merged private commit; the only
graph.pydifferences between the two repos are the monetisation W3 reasoning-quota lines, which are private-only and untouched here.Rollback
Revert the single commit.
headless.pyis a new module and all wiring is flag-gated, so reverting restores exact prior behaviour._chunk_content_hashis an additive node property ignored by every prior version — no data-loss risk.🤖 Generated with Claude Code