This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new b029b17dfb Converge TODO-tracker scripts to be repo-agnostic across
juneau/release-manager/support-console
b029b17dfb is described below
commit b029b17dfb0294d4b20c8948a3f42ae51a9a46f9
Author: James Bognar <[email protected]>
AuthorDate: Tue Aug 18 14:47:15 2026 -0400
Converge TODO-tracker scripts to be repo-agnostic across
juneau/release-manager/support-console
Harden scripts/todo-next-id.py and scripts/todo-status-audit.py so all three
repos' copies are byte-identical below their per-repo constants:
- Add --root and --allow-missing flags
- Exit 2 on a missing tracker dir instead of silently printing 1
- Emit a scan banner to stderr
- Reconcile structure via DEFAULT_REPO_ROOT
Also mark both scripts executable (644 -> 755) to match their
#!/usr/bin/env python3 shebangs and ./scripts/... invocation.
---
scripts/todo-next-id.py | 102 +++++++++++++++++++++++++++++++++----------
scripts/todo-status-audit.py | 77 ++++++++++++++++++++++++--------
2 files changed, 136 insertions(+), 43 deletions(-)
diff --git a/scripts/todo-next-id.py b/scripts/todo-next-id.py
old mode 100644
new mode 100755
index d99f9c803c..5a74ec73f7
--- a/scripts/todo-next-id.py
+++ b/scripts/todo-next-id.py
@@ -12,13 +12,28 @@
# * specific language governing permissions and limitations under the License.
#
***************************************************************************************************************************
"""
-Next-free TODO-id calculator for Apache Juneau's .work/todo/ tracker.
+Next-free TODO-id calculator for this repository's .work/todo/ tracker.
-Mirrors the exact scan scope documented in
agents/skills/juneau-todo-management/SKILL.md's
+Repo-agnostic: the repository root is derived from this file's own location
+(<root>/scripts/todo-next-id.py -> <root>), never hardcoded, so the same body
works in
+every repository that adopts the convention. Only the REPO_LABEL / SKILL_NAME
constants
+below and the license header differ between copies.
+
+**Ids are per-repository and start at 1 in each.** This script only ever scans
the tree it
+lives in. It prints the resolved root to stderr on every run precisely so that
a run made
+from the wrong working tree is visible rather than silent.
+
+Mirrors the exact scan scope documented in this repo's TODO-management skill,
in its
"Adding a new item" and "MAYBE Numbering" sections:
1. Every "[TODO-n]" and bare "TODO-n" token in .work/todo/TODO.md (a
trailing lowercase
- letter suffix, e.g. "TODO-174a", is stripped -- only the numeric part
counts).
+ letter suffix, e.g. "TODO-174a", is stripped -- only the numeric part
counts). A
+ qualified cross-repo citation such as "juneau:TODO-42" is NOT counted; it
names an id in
+ another repo's tracker.
+
+ Note that this scan cannot distinguish an illustrative id from a live
one: writing
+ "for example, TODO-5" anywhere in TODO.md permanently consumes id 5.
Write "TODO-<n>" in
+ prose.
2. Every
"TODO-"/"READY-"/"MAYBE-"/"FINISHED-"/"CANCELLED-<n>[<letter>]-*.md" filename
directly under .work/todo/ and .work/todo/finished/.
@@ -26,25 +41,32 @@ next = 1 + max(all numeric ids found). A child's letter
suffix (TODO-174a, FINIS
is ignored for this computation -- only its numeric part counts, per the
skill's "Numbering"
rule -- so promoting/splitting a lettered child never consumes a new
sequential id.
-.work/ is gitignored in apache/juneau, so this is pure filesystem/text
scanning; no git needed.
+.work/ is gitignored, so this is pure filesystem/text scanning; no git needed.
+
+A MISSING .work/todo/ directory is a hard error (exit 2), not an empty scan.
Silently
+returning "1" from a tree that has no tracker is how ids get reused: it is the
correct answer
+in a freshly-seeded repo and a catastrophic one in a repo whose tracker you
failed to find.
+Pass --allow-missing if you genuinely want the empty-tree answer.
Usage:
./scripts/todo-next-id.py
./scripts/todo-next-id.py --list
- ./scripts/todo-next-id.py --check 174a
+ ./scripts/todo-next-id.py --check 12
+ ./scripts/todo-next-id.py --root /path/to/other/repo
Options:
+ --root <path> Repository root to scan (default: the parent of this
script's directory).
--list Print every id currently in use (letter suffixes
preserved), one per
line, sorted numerically then by letter, instead of the
next free id.
- --check <id> Exit 1 with a message if <id> (e.g. "347" or "174a"; a
leading "TODO-"
+ --check <id> Exit 1 with a message if <id> (e.g. "12" or "7a"; a
leading "TODO-"
is tolerated) is already in use; exit 0 with a message if
it's free.
+ --allow-missing Treat an absent .work/todo/ as an empty tracker instead of
an error.
--help, -h Show this help message.
-Examples:
- ./scripts/todo-next-id.py
- ./scripts/todo-next-id.py --list
- ./scripts/todo-next-id.py --check 347
- ./scripts/todo-next-id.py --check TODO-174a
+Exit status:
+ 0 Success (or --check found the id free).
+ 1 --check found the id already taken, or a malformed --check argument.
+ 2 .work/todo/ does not exist under the resolved root (and
--allow-missing was not given).
"""
from __future__ import annotations
@@ -54,40 +76,50 @@ import re
import sys
from pathlib import Path
-REPO_ROOT = Path(__file__).resolve().parent.parent
-TODO_DIR = REPO_ROOT / ".work" / "todo"
-FINISHED_DIR = TODO_DIR / "finished"
+#
---------------------------------------------------------------------------------------
+# The ONLY repo-specific values in this file. Everything below is identical
across every
+# copy of this script; keep it that way so a fix lands once and is copied
verbatim.
+#
---------------------------------------------------------------------------------------
+REPO_LABEL = "Apache Juneau"
+SKILL_NAME = "juneau-todo-management"
+
+DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
# "[TODO-42]" or bare "TODO-42" in TODO.md prose. A trailing run of lowercase
letters (the
-# child-letter suffix, possibly more than one for grandchildren like "175fa")
is captured
+# child-letter suffix, possibly more than one for grandchildren like "17fa")
is captured
# separately so it can be preserved for --list/--check but ignored for
numbering.
-TODO_TOKEN_RE = re.compile(r"\bTODO-(\d+)([a-z]*)\b")
+#
+# The leading lookbehind rejects a qualified cross-repo citation --
"juneau:TODO-42",
+# "support-console:TODO-42" -- which names an id in ANOTHER repo's tracker and
must not
+# consume one here. Ids are bare and per-repository, so without this the act
of writing down
+# that another repo's item blocks you would silently burn a local id.
+TODO_TOKEN_RE = re.compile(r"(?<![\w:])TODO-(\d+)([a-z]*)\b")
# Every lifecycle-state filename directly under .work/todo/ or
.work/todo/finished/.
FILENAME_RE =
re.compile(r"^(?:TODO|READY|MAYBE|FINISHED|CANCELLED)-(\d+)([a-z]*)-.*\.md$")
-def collect_ids() -> tuple[set, set]:
+def collect_ids(todo_dir: Path) -> tuple[set, set]:
"""
Scan every source described in the module docstring.
Returns (raw_ids, numeric_ids):
- - raw_ids: every distinct id token as it actually appears (e.g.
"174", "174a",
- "312f"), for --list / --check.
+ - raw_ids: every distinct id token as it actually appears (e.g.
"17", "17a",
+ "12f"), for --list / --check.
- numeric_ids: just the base integer part of each id (letter suffix
stripped), for
computing the next free id.
"""
raw_ids = set()
numeric_ids = set()
- todo_md = TODO_DIR / "TODO.md"
+ todo_md = todo_dir / "TODO.md"
if todo_md.is_file():
text = todo_md.read_text(encoding="utf-8")
for m in TODO_TOKEN_RE.finditer(text):
raw_ids.add(m.group(1) + m.group(2))
numeric_ids.add(int(m.group(1)))
- for directory in (TODO_DIR, FINISHED_DIR):
+ for directory in (todo_dir, todo_dir / "finished"):
if not directory.is_dir():
continue
for entry in directory.iterdir():
@@ -123,20 +155,42 @@ def normalize_check_id(raw: str) -> str | None:
def main():
parser = argparse.ArgumentParser(
- description="Compute the next free .work/todo/ TODO id (see
agents/skills/juneau-todo-management/SKILL.md).",
+ description=f"Compute the next free .work/todo/ TODO id for
{REPO_LABEL} (see @{SKILL_NAME}).",
epilog=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
+ parser.add_argument("--root", metavar="PATH", help="Repository root to
scan (default: parent of this script's directory).")
parser.add_argument("--list", action="store_true", help="Print every id
currently in use, sorted.")
parser.add_argument("--check", metavar="ID", help="Exit non-zero if ID is
already taken.")
+ parser.add_argument("--allow-missing", action="store_true", help="Treat an
absent .work/todo/ as empty instead of an error.")
args = parser.parse_args()
- raw_ids, numeric_ids = collect_ids()
+ repo_root = Path(args.root).resolve() if args.root else DEFAULT_REPO_ROOT
+ todo_dir = repo_root / ".work" / "todo"
+
+ # Always announce the tree actually scanned. Ids are per-repository and
bare, so
+ # "TODO-5" is a different item in each repo; a wrong-tree run must not
look identical
+ # to a right-tree one. stderr, so `NEXT=$(./scripts/todo-next-id.py)`
still works.
+ print(f"[{REPO_LABEL}] scanning {todo_dir}", file=sys.stderr)
+
+ if not todo_dir.is_dir():
+ if not args.allow_missing:
+ print(
+ f"ERROR: {todo_dir} does not exist.\n"
+ f" Ids are per-repository, so an unfound tracker must
not be reported as an empty\n"
+ f" one -- that silently hands out id 1 and reuses live
ids. Check you are in the\n"
+ f" right working tree, or pass --allow-missing if this
repo genuinely has no tracker yet.",
+ file=sys.stderr,
+ )
+ return 2
+ print(f"WARNING: {todo_dir} does not exist; treating as empty
(--allow-missing).", file=sys.stderr)
+
+ raw_ids, numeric_ids = collect_ids(todo_dir)
if args.check is not None:
normalized = normalize_check_id(args.check)
if normalized is None:
- print(f"ERROR: '{args.check}' is not a valid id (expected e.g.
'347' or '174a').", file=sys.stderr)
+ print(f"ERROR: '{args.check}' is not a valid id (expected e.g.
'12' or '7a').", file=sys.stderr)
return 1
if normalized in raw_ids:
print(f"TAKEN: {normalized} is already in use.")
diff --git a/scripts/todo-status-audit.py b/scripts/todo-status-audit.py
old mode 100644
new mode 100755
index 176b642337..177260d4a9
--- a/scripts/todo-status-audit.py
+++ b/scripts/todo-status-audit.py
@@ -12,14 +12,19 @@
# * specific language governing permissions and limitations under the License.
#
***************************************************************************************************************************
"""
-Best-effort status/header consistency pre-filter for Apache Juneau's
.work/todo/ plan files.
+Best-effort status/header consistency pre-filter for this repository's
.work/todo/ plan files.
+
+Repo-agnostic: the repository root is derived from this file's own location
+(<root>/scripts/todo-status-audit.py -> <root>), never hardcoded, so the same
body works in
+every repository that adopts the convention. Only the REPO_LABEL / SKILL_NAME
constants
+below and the license header differ between copies.
Checks every TODO-<id>-*.md / READY-<id>-*.md / MAYBE-<id>-*.md file directly
under .work/todo/
-(FINISHED-/CANCELLED-*.md archives are explicitly out of scope -- per
-agents/skills/juneau-todo-management/SKILL.md, "status line is not required in
FINISHED archives")
-against that skill's "Per-file `Current status:` and `Complexity:` header"
rules, and flags
-candidate inconsistencies. This is a PRE-FILTER, not a validator: it flags
candidates for a human
-(or agent) to look at, and will not catch everything on older, format-drifted
files -- tolerant,
+(FINISHED-/CANCELLED-*.md archives are explicitly out of scope -- per this
repo's
+TODO-management skill, "status line is not required in FINISHED archives")
against that
+skill's "Per-file `Current status:` and `Complexity:` header" rules, and flags
candidate
+inconsistencies. This is a PRE-FILTER, not a validator: it flags candidates
for a human
+(or agent) to look at, and will not catch everything on format-drifted files
-- tolerant,
best-effort markdown-header parsing throughout.
Checks performed (each file may accumulate multiple flags):
@@ -47,20 +52,29 @@ Checks performed (each file may accumulate multiple flags):
wording is reserved for MAYBE-*.md files).
- maybe_prefix_non_parked A MAYBE-*.md file whose status does NOT start
with "Parked".
+A MISSING scan directory is a hard error (exit 2). An EMPTY-but-present one is
a clean pass
+(exit 0). The original version conflated the two and returned 0 for both, so
pointing the
+script at a tree with no tracker produced a reassuring "nothing to flag" --
the same silent-zero
+trap as running `rg` over the gitignored .work/ without --no-ignore.
+
Usage:
./scripts/todo-status-audit.py
./scripts/todo-status-audit.py --verbose
+ ./scripts/todo-status-audit.py --root /path/to/other/repo
./scripts/todo-status-audit.py --dir /path/to/alternate/todo/dir
Options:
- --dir <path> Directory to scan (default: .work/todo/ under the repo
root). Non-recursive --
- only *.md files directly in this directory are considered.
+ --root <path> Repository root; scans <root>/.work/todo/ (default: parent
of this
+ script's directory). Ignored if --dir is given.
+ --dir <path> Exact directory to scan, overriding --root. Non-recursive
-- only *.md
+ files directly in this directory are considered.
--verbose, -v Also print files that passed every check (default: only
print flagged files).
--help, -h Show this help message.
Exit status:
- 0 No inconsistencies flagged.
+ 0 No inconsistencies flagged (including the legitimately-empty-tracker
case).
1 At least one file was flagged.
+ 2 The scan directory does not exist.
"""
from __future__ import annotations
@@ -70,8 +84,14 @@ import re
import sys
from pathlib import Path
-REPO_ROOT = Path(__file__).resolve().parent.parent
-DEFAULT_TODO_DIR = REPO_ROOT / ".work" / "todo"
+#
---------------------------------------------------------------------------------------
+# The ONLY repo-specific values in this file. Everything below is identical
across every
+# copy of this script; keep it that way so a fix lands once and is copied
verbatim.
+#
---------------------------------------------------------------------------------------
+REPO_LABEL = "Apache Juneau"
+SKILL_NAME = "juneau-todo-management"
+
+DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
FILENAME_RE = re.compile(r"^(TODO|READY|MAYBE)-\d+[a-z]*-.*\.md$")
@@ -80,7 +100,11 @@ COMPLEXITY_LINE_RE = re.compile(r"^\s*Complexity:\s*(.*)$",
re.IGNORECASE | re.M
SECTION_HEADING_RE = re.compile(r"^##\s+(.*)$", re.MULTILINE)
NUMBERED_ITEM_RE = re.compile(r"^\s*\d+[.)]\s+(.*)$", re.MULTILINE)
-RESOLVED_MARKERS = ("resolved", "answered", "decided")
+# Matched on word boundaries. A bare substring test reads "unanswered" /
"unresolved" -- the
+# most natural wording for an OPEN question -- as containing "answered" /
"resolved", which
+# silently disables the ready_but_has_open_questions check for exactly the
case it exists to
+# catch. Still best-effort: an explicit negation like "not resolved" reads as
resolved.
+RESOLVED_MARKER_RE = re.compile(r"\b(?:resolved|answered|decided)\b")
# Recognized status prefixes (case-insensitive, checked with str.startswith
after lowercasing) per
# the skill's "Status wording rules" -- kept separate from TODO/READY vs MAYBE
since the two file
@@ -95,8 +119,6 @@ MAYBE_STATUS_PREFIX = "parked"
def find_plan_files(todo_dir: Path) -> list:
"""Every TODO-/READY-/MAYBE-<id>[<letter>]-*.md file directly under
todo_dir, sorted by name."""
- if not todo_dir.is_dir():
- return []
return sorted(p for p in todo_dir.glob("*.md") if
FILENAME_RE.match(p.name))
@@ -131,7 +153,7 @@ def open_questions_are_unresolved(section_body: str) ->
bool:
start = m.start()
end = items[i + 1].start() if i + 1 < len(items) else len(section_body)
block = section_body[start:end].lower()
- if not any(marker in block for marker in RESOLVED_MARKERS):
+ if not RESOLVED_MARKER_RE.search(block):
return True
return False
@@ -190,19 +212,36 @@ def audit_file(path: Path) -> list:
def main() -> int:
parser = argparse.ArgumentParser(
- description="Best-effort pre-filter for .work/todo/ Current
status:/Complexity: header inconsistencies.",
+ description=f"Best-effort pre-filter for {REPO_LABEL}'s .work/todo/
header inconsistencies (see @{SKILL_NAME}).",
epilog=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
- parser.add_argument("--dir", metavar="PATH", help="Directory to scan
(default: .work/todo/ under the repo root).")
+ parser.add_argument("--root", metavar="PATH", help="Repository root; scans
<root>/.work/todo/ (default: parent of this script's directory).")
+ parser.add_argument("--dir", metavar="PATH", help="Exact directory to
scan, overriding --root.")
parser.add_argument("--verbose", "-v", action="store_true", help="Also
print files that passed every check.")
args = parser.parse_args()
- todo_dir = Path(args.dir) if args.dir else DEFAULT_TODO_DIR
+ if args.dir:
+ todo_dir = Path(args.dir).resolve()
+ else:
+ repo_root = Path(args.root).resolve() if args.root else
DEFAULT_REPO_ROOT
+ todo_dir = repo_root / ".work" / "todo"
+
+ # Always announce the tree actually scanned, for the same reason
todo-next-id.py does:
+ # bare per-repository ids make a wrong-tree run indistinguishable from a
right-tree one.
+ print(f"[{REPO_LABEL}] scanning {todo_dir}", file=sys.stderr)
+
+ # A missing directory and an empty one are NOT the same answer. Missing
means the caller
+ # is looking at the wrong tree (or the scaffolding was never installed)
and must be told;
+ # empty means a genuinely clean tracker and is a legitimate pass.
+ if not todo_dir.is_dir():
+ print(f"ERROR: {todo_dir} does not exist -- nothing was scanned. Check
the working tree, or pass --dir.", file=sys.stderr)
+ return 2
+
files = find_plan_files(todo_dir)
if not files:
- print(f"No TODO-/READY-/MAYBE-*.md files found under {todo_dir}.")
+ print(f"No TODO-/READY-/MAYBE-*.md files found under {todo_dir}
(directory exists and is empty of plan files).")
return 0
flagged_count = 0