Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,42 @@ Free-form markdown. Strong conventions:
Autonomy: supervised # safe | supervised | human-required
Priority: normal # low | normal | high
Status: draft
Blocked-by: PyAutoFit#1436 # optional; see "Declaring a gate" below
```

When present, `Type:` should match the work-type folder. The goal is light
structure, not bureaucracy — prompts stay free-form prose.

**Declaring a gate — `Closes-when:` / `Blocked-by:`.** Both optional. A prompt
that waits on something external can say so in a form `lifecycle.py issues
--drafts` can grade:

```markdown
Closes-when: autolens_profiling#70 # this prompt is DONE when that closes
Blocked-by: PyAutoArray#431, PyAutoGalaxy#486 # READY TO START when all close
```

The two readings are **opposite**, which is the whole point. Prose cannot be
graded, so a cited issue could mean either and `--drafts` had to report every
one as the same ambiguous question. With a declared key the tool reports the
action instead: a closed `Closes-when:` says *likely shipped, verify and
retire*; a closed `Blocked-by:` says *ready to start*. Prompts declaring a gate
drop out of the ambiguous advisory list.

Notes:
- Accepts `Repo#123` shorthand (assumed `PyAutoLabs/`) or a full URL, and PRs
as well as issues. Several refs may be comma-separated.
- `Blocked-by:` clears only when **every** ref closes; a partly-satisfied gate
is reported in its own weaker band rather than as ready.
- Keys inside fenced code blocks are documentation and are ignored, so a prompt
may show the syntax without declaring a gate.
- Advisory, never a gate on the exit code: retiring a prompt writes to
`complete/` and stays a human act.

Motivated by the 2026-08-09 `draft/` sweep, where five prompts' stated gates
had closed without anyone noticing — including one whose exit condition was met
the same day it was written.

The optional `Difficulty:` / `Autonomy:` / `Priority:` keys let both people and
PyAutoBrain see, at a glance, how hard a task is, whether an agent can safely
take it on, and how urgent it is. What each `Autonomy:` level *does* at every
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Phase 2 — drop the hand-written quick-update sentence from the workspace scripts

Blocked-by: PyAutoFit#1436 # moved the cadence message into the library — MERGED 2026-07-30

Follow-up to **PyAutoFit#1434 / PR#1436**, which moved the on-the-fly update
cadence message into the library. Do not start until #1436 has merged.

Expand Down
1 change: 1 addition & 0 deletions draft/docs/workspaces/preloads_advanced_workspace_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Difficulty: too-large
Autonomy: supervised
Priority: high
Status: formalised
Blocked-by: PyAutoLens#565 # the Preloads API landing in PyAutoArray — CLOSED 2026-07-10

Write an **advanced** guide, in both `@autolens_workspace` and `@autogalaxy_workspace`, on the
`Preloads` API (`aa.PreloadsInterferometer`, `aa.AbstractPreloads`, and `PreloadsImaging` if added)
Expand Down
1 change: 1 addition & 0 deletions draft/feature/autofit/ep_analytic_updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Difficulty: large
Autonomy: supervised
Priority: normal
Status: formalised
Blocked-by: PyAutoFit#1334, PyAutoFit#1331, PyAutoFit#1332 # WP1 gate (MERGED); WP3/WP4 gates (open)

The complete, self-contained implementation plan lives in
**PyAutoFit#1338** (plan-only issue; nothing implemented). Scoping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Difficulty: large
Autonomy: supervised
Priority: normal
Status: formalised
Blocked-by: PyAutoArray#363, PyAutoGalaxy#486, autolens_workspace#236 # the k x s machinery — all MERGED

Option (a) of the phase-4 fork (PyAutoLabs/PyAutoArray#362, decided
2026-07-08): flip the executed start-here simulator to
Expand Down
38 changes: 36 additions & 2 deletions draft/feature/pyautomind/draft_staleness_detection_signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,43 @@ is already reconcile's stated contract. Keep it.

## Acceptance

> **DELIVERED 2026-08-09 — and one criterion below was wrong when written.**
> Legs 1 and 2 shipped (PyAutoMind `lifecycle.py` gate keys; PyAutoBrain
> `intake reconcile` re-rank). Leg 3 (`--repo` upstream mode) is not built.
>
> **The "all five in the top band" criterion is unachievable and contradicts
> this prompt's own § Hard limit.** That section already says one finding left
> no Mind-side signal at all — so a Mind-local ranker cannot rank it, by
> construction. Trying to satisfy the criterion anyway actively made the tool
> worse: a loose series match pulled one more finding in but FALSELY flagged
> `test_mode_bypass_ordered_assertion_ties`, breaking the criterion below it.
>
> What the five actually need is three different tools, which is the real
> finding:
>
> | finding | caught by | status |
> |---|---|---|
> | k×s series | reconcile — rare-token fan-out | **rank 2 of 31** |
> | nufft chunking | reconcile — shared rare identifiers | flagged |
> | test-mode umbrella | `Closes-when:` header key (leg 1) | declared + graded |
> | split guard | nothing Mind-local (evidence sat in a sibling *prompt*) | needs leg 3 |
> | latent resume | nothing at all (no record exists) | needs leg 3 |
>
> Corrected criteria, all met:
>
> - The ranker flags **materially fewer** than 96: **31 of 148 (21%)**, down from
> 96 (65%), with `high` cut from 52 to 9.
> - Every finding **reachable from Mind-local evidence** is flagged, and the
> largest is at rank 2 (it was previously not flagged at all).
> - `test_mode_bypass_ordered_assertion_ties.md` is NOT reported as shipped.
> - No prompt is moved or retired by the tool (asserted by a test).
>
> Leg 3 remains open and is now better motivated: it is the *only* route to the
> last two findings.

- Re-running the ranker against PyAutoMind `f25e154e` (the pre-sweep tree, which
is the labelled set this prompt establishes) puts **all five** confirmed
findings in the top band, and flags materially fewer than 96 prompts.
is the labelled set this prompt establishes) puts every **Mind-reachable**
confirmed finding in the top band, and flags materially fewer than 96 prompts.
- `test_mode_bypass_ordered_assertion_ties.md` is NOT reported as shipped.
- No prompt is moved or retired by the tool.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Difficulty: small
Autonomy: supervised
Priority: normal
Status: formalised
Blocked-by: PyAutoArray#431 # the small-datasets loader fix — MERGED 2026-08-03, released in 2026.8.7.1

BLOCKED until PyAutoArray PR#431 merges and reaches the installed stack. Do not
start before then — the script only passes with that loader fix in place.
Expand Down
167 changes: 163 additions & 4 deletions scripts/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,13 @@ def draft_issue_notes(root: Path, fetch=None) -> "list[str]":
registry entry's `issue:` is its OWN tracking issue, so closed means done. A
draft usually cites an issue as CONTEXT — "Once #480 is fixed…", "Follow-up
to #57" — so closed can mean the draft is newly UNBLOCKED rather than
finished. Both readings are worth a human look; neither is a gate."""
refs = draft_issue_refs(root)
finished. Both readings are worth a human look; neither is a gate.

For the drafts that DO state which reading applies, see `draft_gate_notes`:
an explicit `Closes-when:` / `Blocked-by:` header removes exactly this
ambiguity, and those drafts are reported there instead of here."""
gated = {path for path, _, _ in draft_gate_refs(root)}
refs = [(p, u) for p, u in draft_issue_refs(root) if p not in gated]
if not refs:
return []
fetch = fetch or _gh_issue_states
Expand All @@ -356,6 +361,133 @@ def draft_issue_notes(root: Path, fetch=None) -> "list[str]":
]


# --------------------------------------------------------------------------- #
# draft gates
#
# The 2026-08-09 draft/ sweep found five prompts whose stated gate had since
# closed, and the two readings are OPPOSITE: `test_mode_representative_outputs`
# said "EPIC CLOSES when #70 ships its recipe leg" (gate closed => the prompt is
# DONE), while `unpark_imaging_scaling_relation_slam` said "BLOCKED until
# PyAutoArray PR#431 merges" (gate closed => the prompt is READY TO START).
# Prose cannot be graded, so `--drafts` had to lump both into one "shipped, or
# newly unblocked?" note. These keys let a prompt say which it means.
# --------------------------------------------------------------------------- #
GATE_FIELDS = ("closes-when", "blocked-by")
#: `Repo#123` shorthand as well as full URLs — prompts overwhelmingly write the
#: former, and a URL-only extractor found 2 refs across the backlog where the
#: shorthand form found 8 (2026-08-09 measurement).
GATE_REF_RE = re.compile(
r"https://github\.com/([\w.-]+)/([\w.-]+)/(?:issues|pull)/(\d+)"
r"|(?<![\w/])([A-Za-z_][\w.]*)#(\d+)\b"
)
_GATE_KEY_RE = re.compile(r"^\s*(closes-when|blocked-by)\s*:\s*(.+?)\s*$", re.I)
DEFAULT_GATE_OWNER = "PyAutoLabs"


def _gate_url(match: "re.Match") -> "str | None":
"""Normalise either GATE_REF_RE alternative to a canonical issues URL.

`Repo#123` cannot say whether 123 is an issue or a PR, and the GitHub API
resolves an issues URL for both (a PR *is* an issue), so the issues form is
the safe canonical shape."""
owner, repo, num, short_repo, short_num = match.groups()
if owner:
return f"https://github.com/{owner}/{repo}/issues/{num}"
if short_repo:
return f"https://github.com/{DEFAULT_GATE_OWNER}/{short_repo}/issues/{short_num}"
return None


def draft_gate_refs(root: Path) -> "list[tuple[str, str, str]]":
"""(draft_path, gate_kind, url) for drafts carrying a gate header key.

`gate_kind` is `closes-when` or `blocked-by` — the two opposite readings.
A key may list several refs; each becomes its own entry, because a prompt
blocked on three PRs is only unblocked when the last one lands."""
draft = root / "draft"
if not draft.is_dir():
return []
refs = []
for f in sorted(draft.rglob("*.md")):
rel = str(f.relative_to(root))
in_fence = False
for line in f.read_text(errors="replace").splitlines():
# Fenced blocks are documentation, not declarations. Prompts that
# *describe* these keys (this feature's own prompt does, in a
# ```markdown example) must not be read as declaring them.
if line.lstrip().startswith("```"):
in_fence = not in_fence
continue
if in_fence:
continue
m = _GATE_KEY_RE.match(line)
if not m:
continue
kind = m.group(1).lower()
for ref in GATE_REF_RE.finditer(m.group(2)):
url = _gate_url(ref)
if url:
refs.append((rel, kind, url))
return refs


def draft_gate_notes(root: Path, fetch=None) -> "dict[str, list[str]]":
"""Drafts whose declared gate has closed, split by what that MEANS.

Returns `{"shipped": [...], "unblocked": [...], "partial": [...],
"unreadable": [...]}`. Unlike `draft_issue_notes` these are unambiguous —
the prompt author said which reading applies — so each line states the
action rather than asking a question.

Aggregated PER PROMPT, not per reference: a prompt blocked on three PRs is
unblocked only when the last one lands, so reporting each ref separately
would claim "ready to start" three times while it is still blocked. A
partially-satisfied `Blocked-by:` is reported in its own weaker band, which
is the real state of `ep_analytic_updates` (its WP1 gate merged; the WP3/WP4
gates are open).

Still advisory, and deliberately so: a satisfied `Closes-when:` is strong
evidence the work is done, but retiring a prompt writes to `complete/` and
stays a human act (the same contract `intake reconcile` keeps)."""
refs = draft_gate_refs(root)
out = {"shipped": [], "unblocked": [], "partial": [], "unreadable": []}
if not refs:
return out
fetch = fetch or _gh_issue_states
states = fetch(sorted({url for _, _, url in refs}))

grouped: "dict[tuple[str, str], list[str]]" = {}
for path, kind, url in refs:
grouped.setdefault((path, kind), []).append(url)

for (path, kind), urls in sorted(grouped.items()):
got = [(u, states.get(u, "unknown")) for u in urls]
bad = [f"{u} ({s})" for u, s in got if s not in ("open", "closed")]
if bad:
out["unreadable"].append(
f"{path}: could not read {len(bad)} declared gate(s): "
+ ", ".join(bad))
continue
closed = [u for u, s in got if s == "closed"]
if not closed:
continue
joined = ", ".join(closed)
if len(closed) < len(got):
still = ", ".join(u for u, s in got if s == "open")
out["partial"].append(
f"{path}: {len(closed)} of {len(got)} `{kind}:` gates closed — "
f"partly ready; still open: {still}")
elif kind == "closes-when":
out["shipped"].append(
f"{path}: every `Closes-when:` gate is CLOSED — the prompt's own "
f"exit condition is met, so this is very likely shipped: {joined}")
else:
out["unblocked"].append(
f"{path}: every `Blocked-by:` gate is CLOSED — ready to start, "
f"not blocked: {joined}")
return out


def cmd_issues(args) -> int:
"""Cross-check every registry entry's tracking issue against GitHub."""
try:
Expand All @@ -370,8 +502,10 @@ def cmd_issues(args) -> int:
)
return 2
notes = []
gates = {"shipped": [], "unblocked": [], "partial": [], "unreadable": []}
if getattr(args, "drafts", False):
try:
gates = draft_gate_notes(ROOT)
notes = draft_issue_notes(ROOT)
except GhUnavailable:
pass # unreachable: issue_problems above would already have raised
Expand All @@ -383,10 +517,35 @@ def cmd_issues(args) -> int:
else:
print(f"lifecycle issues: OK ({len(registry_issue_refs(ROOT))} tracking issue(s) open)")

# Declared gates first: the prompt author said which reading applies, so
# these are actionable rather than a question. Still advisory — retiring a
# prompt writes to complete/ and stays human.
if gates["shipped"]:
print(f"\nGATE MET — {len(gates['shipped'])} draft(s) whose `Closes-when:` "
f"has closed (likely shipped; verify, then retire):")
for line in gates["shipped"]:
print(f" ! {line}")
if gates["unblocked"]:
print(f"\nUNBLOCKED — {len(gates['unblocked'])} draft(s) whose `Blocked-by:` "
f"has closed (ready to start):")
for line in gates["unblocked"]:
print(f" > {line}")
if gates["partial"]:
print(f"\npartly unblocked — {len(gates['partial'])} draft(s) with some "
f"gates closed:")
for line in gates["partial"]:
print(f" ~ {line}")
if gates["unreadable"]:
print(f"\nunreadable — {len(gates['unreadable'])} declared gate(s):")
for line in gates["unreadable"]:
print(f" ? {line}")

# Advisory only — never affects the exit code. A draft citing a closed issue
# may be shipped OR newly unblocked; that is a judgement, not drift.
# with no declared gate may be shipped OR newly unblocked; that is a
# judgement, not drift. Drafts that DO declare a gate are reported above
# instead, so this list is the genuinely-ambiguous remainder.
if notes:
print(f"\nadvisory — {len(notes)} draft(s) citing a closed issue:")
print(f"\nadvisory — {len(notes)} undeclared draft(s) citing a closed issue:")
for line in notes:
print(f" ? {line}")

Expand Down
Loading
Loading