Skip to content

Commit 282b59f

Browse files
Rylaaclaude
andcommitted
fix: v0.13.2 — polish from the release review
Four small items the fresh-eyes verifier flagged as post-release debt, none of them blocking, all cheap enough to land together. - `LEDGER*.md` is a glob, not a prefix free-for-all: "ledger" must now be a whole name segment, so `ledgers.md` and `ledgerish.md` no longer count while `LEDGER.md`, `LEDGER-topic.md`, `LEDGER_notes.md` and the lowercase spellings still do. - README's two guard-flow diagrams said `.workflow/LEDGER.md`; any active `LEDGER*.md` passes them. - The monotonic-clock AST guard covered plain, annotated and variable-indirected assignments but not tuple unpacking (`_pad, deadline = 0, time.time() + B`) or the walrus operator. Both now covered and mutation-checked; neither form appears in the code today, so this is a guard against a future edit rather than a fix. - Two comments still described the budgets as wall-clock. - tests 129 -> 131. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rsqc71VgbiJx2vpsZyLEbz
1 parent 1156e31 commit 282b59f

6 files changed

Lines changed: 48 additions & 6 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "orchestrator",
33
"description": "Token-frugal multi-agent orchestration for Claude Code, built for a Claude Fable 5 chair: Fable plans and decides, Sonnet 5 carries implementation and routine judgment, and a two-tier verification/escalation valve guards the close. When the Fable limit runs dry, an Opus fallback profile keeps the same discipline running. A Requirements Ledger and guard hooks enforce the discipline mechanically, and finished tmux teammates are reaped automatically.",
4-
"version": "0.13.1",
4+
"version": "0.13.2",
55
"author": {
66
"name": "Yusuf Demirkoparan",
77
"url": "https://github.com/Rylaa"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ spawn (Agent / Task / Workflow)
110110
111111
├─ subagent_type == "fork" ................. PASS (forks already see the ledger)
112112
113-
├─ ACTIVE .workflow/LEDGER.md found ........ PASS (cite its items per agent)
113+
├─ ACTIVE .workflow/LEDGER*.md found ....... PASS (cite its items per agent)
114114
│ (cwd → repo root / $HOME)
115115
116116
└─ no ledger — or only a stale one ......... DENY → "write the ledger first;
@@ -124,7 +124,7 @@ A ledger is *stale* when every item is closed AND it was last touched before thi
124124
```
125125
TaskCreate (tracker task)
126126
127-
├─ ACTIVE .workflow/LEDGER.md found .............. PASS
127+
├─ ACTIVE .workflow/LEDGER*.md found ............. PASS
128128
├─ fewer than 3 ledgerless tasks this session .... PASS (small task lists are fine)
129129
130130
└─ 3rd ledgerless task ........................... DENY once → "multi-phase work:

scripts/ledger_guard_spawn.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ def active_ledger_in(dirpath):
236236
low = name.lower()
237237
if not (low.startswith("ledger") and low.endswith(".md")):
238238
continue
239+
# "ledger" must be a whole segment: LEDGER.md, LEDGER-topic.md,
240+
# LEDGER_topic.md — but not ledgers.md or ledgerish.md.
241+
if low[6:7] not in (".", "-", "_"):
242+
continue
239243
# Retired only when "archive" is the trailing segment, the form
240244
# this hook's own message asks for. A live ledger ABOUT archives
241245
# (LEDGER-archive-migration.md) must still count.

scripts/ledger_guard_stop.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ def active_ledger_in(dirpath):
9393
low = name.lower()
9494
if not (low.startswith("ledger") and low.endswith(".md")):
9595
continue
96+
# "ledger" must be a whole segment: LEDGER.md, LEDGER-topic.md,
97+
# LEDGER_topic.md — but not ledgers.md or ledgerish.md.
98+
if low[6:7] not in (".", "-", "_"):
99+
continue
96100
# Retired only when "archive" is the trailing segment, the form
97101
# this hook's own message asks for. A live ledger ABOUT archives
98102
# (LEDGER-archive-migration.md) must still count.

tests/test_spawn_guard.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,28 @@ def test_lowercase_ledger_name_counts(repo_dir):
159159
assert run_hook(SCRIPT, spawn_payload(repo_dir, prompt=VERY_LONG)) is None
160160

161161

162+
def test_ledger_must_be_a_whole_name_segment(repo_dir):
163+
# `LEDGER*.md` is a glob, not a prefix free-for-all: a file merely
164+
# starting with the letters must not be mistaken for a ledger.
165+
for name in ("ledgers.md", "ledgerish.md", "LEDGERS.md"):
166+
d = repo_dir / ".workflow"
167+
d.mkdir(parents=True, exist_ok=True)
168+
for old in d.glob("*.md"):
169+
old.unlink()
170+
(d / name).write_text("- [ ] 1. item\n", encoding="utf-8")
171+
assert is_deny(run_hook(SCRIPT, spawn_payload(repo_dir))), name
172+
173+
174+
def test_separator_forms_all_count(repo_dir):
175+
for name in ("LEDGER.md", "LEDGER-topic.md", "LEDGER_notes.md", "ledger.md"):
176+
d = repo_dir / ".workflow"
177+
d.mkdir(parents=True, exist_ok=True)
178+
for old in d.glob("*.md"):
179+
old.unlink()
180+
(d / name).write_text("- [ ] 1. item\n", encoding="utf-8")
181+
assert run_hook(SCRIPT, spawn_payload(repo_dir, prompt=VERY_LONG)) is None, name
182+
183+
162184
def test_archive_suffixed_ledger_does_not_satisfy_the_gate(repo_dir):
163185
# Renaming to *-archive.md is the documented way to retire a ledger;
164186
# an archived one must not keep the gates disarmed.

tests/test_stop_guard.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,25 @@ def clocks_read_in(node):
450450
# time.time() + N` is the same bug wearing a type hint.
451451
def bindings(t):
452452
for node in ast.walk(t):
453-
if isinstance(node, ast.Assign) and len(node.targets) == 1 \
454-
and isinstance(node.targets[0], ast.Name):
455-
yield node, node.targets[0].id, node.value
453+
if isinstance(node, ast.Assign) and len(node.targets) == 1:
454+
tgt = node.targets[0]
455+
if isinstance(tgt, ast.Name):
456+
yield node, tgt.id, node.value
457+
elif isinstance(tgt, (ast.Tuple, ast.List)) \
458+
and isinstance(node.value, (ast.Tuple, ast.List)) \
459+
and len(tgt.elts) == len(node.value.elts):
460+
# `a, deadline = 0, time.time() + B` — pair them up.
461+
for name_node, val in zip(tgt.elts, node.value.elts):
462+
if isinstance(name_node, ast.Name):
463+
yield node, name_node.id, val
456464
elif isinstance(node, ast.AnnAssign) \
457465
and isinstance(node.target, ast.Name) \
458466
and node.value is not None:
459467
yield node, node.target.id, node.value
468+
elif isinstance(node, ast.NamedExpr) \
469+
and isinstance(node.target, ast.Name):
470+
# `if (deadline := time.time() + B) ...`
471+
yield node, node.target.id, node.value
460472

461473
from_clock = {}
462474
for _node, target, value in bindings(tree):

0 commit comments

Comments
 (0)