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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adept",
"version": "4.7.1",
"version": "4.7.2",
"description": "Development-workflow skills: design, TDD, adversarial review, shipping, and campaign orchestration for Claude Code and Codex.",
"author": {
"name": "David Christensen"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

Accepted (2026-08-12)

> **Amended (2026-09-10):** A canonical `Blocked by #N` record may carry a non-empty
> explanation after the exact ` — ` delimiter. The record must still begin at the start of
> a line, contain one blocker, and fail closed when malformed. The occupancy and
> revalidation decisions otherwise stand as written.

## Context

Issue #56 asks for two things: a `$seek-quest` skill that recommends the next
Expand Down
10 changes: 9 additions & 1 deletion skills/bounty/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,15 @@ operator confirmation.
report line. Birth labels come from the caller's
per-entry state, overriding step 5: `status:blocked` + a `Blocked by #<n>` body line
for dependents, `status:needs-triage` for open-question entries (blocked wins when
both apply), else `status:ready` — the same rule recovery applies below.
both apply), else `status:ready` — the same rule recovery applies below. Prefer the bare
dependency record. When its rationale must remain on the same line, use the exact ` — `
delimiter and non-empty prose. Use one record per blocker, with no leading whitespace;
arbitrary trailing prose and combined references are malformed:

```text
Blocked by #123
Blocked by #123 — the schema change must land first
```

**Epic-parent recovery.** When the parent carries the `epic` label, its Decomposition
section is the authoritative sub-issue list. Enumerate existing native sub-issues
Expand Down
21 changes: 14 additions & 7 deletions skills/quest-log/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,21 @@ Rules:
blocker is open or cannot be resolved. Exit is the canonical cleared-dependency edge
below; explicit `$sort-board` and `$quest` remain manual fallback edges.
- **Cleared-dependency exit edge.** An open, non-epic issue carrying `status:blocked` moves
to `status:ready` only when its body has at least one canonical whole-line
`Blocked by #N` record and every referenced issue resolves closed. `$return-to-town` is
to `status:ready` only when its body has at least one canonical `Blocked by #N`
record and every referenced issue resolves closed. `$return-to-town` is
the primary owner after a verified merge and closure. `$resurrection` owns the same
repair edge behind its plan-and-confirm gate. A canonical line is case-sensitive, has no
leading or trailing content, and contains decimal digits after `#`. A line beginning
exactly `Blocked by #` but failing that grammar is malformed and holds the issue blocked;
other prose and every comment are ignored. Open, missing, malformed, or unreadable
references fail closed and produce an actionable report.
repair edge behind its plan-and-confirm gate. A canonical record is case-sensitive, starts
at the beginning of a line, and contains decimal digits after `#`. It either ends after the
issue number or carries a non-empty explanation after the exact delimiter ` — `. Each
blocker gets its own record. A line beginning exactly `Blocked by #` but failing that
grammar is malformed and holds the issue blocked; other prose and every comment are
ignored. Open, missing, malformed, or unreadable references fail closed and produce an
actionable report. Writers prefer the bare form; readers accept both:

```text
Blocked by #123
Blocked by #123 — the schema change must land first
```

### Recipe: reconcile cleared dependencies

Expand Down
5 changes: 3 additions & 2 deletions skills/quest-log/assets/cleared-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ cleared_dependency_body_verdict() { # repo number body
cleared_dependency_reason=
cleared_dependency_error=false
while IFS= read -r line; do
if [[ $line =~ ^Blocked\ by\ \#([0-9]+)$ ]]; then
if [[ $line =~ ^Blocked\ by\ \#([0-9]+)(\ —\ [^[:space:]].*)?$ ]]; then
blockers+=("${BASH_REMATCH[1]}")
elif [[ $line == 'Blocked by #'* ]]; then
cleared_dependency_reason="malformed reference on #$number; expected Blocked by #N"
cleared_dependency_reason="malformed reference on #$number;"
cleared_dependency_reason+=" expected Blocked by #N or Blocked by #N — explanation"
cleared_dependency_error=true
return 1
fi
Expand Down
3 changes: 2 additions & 1 deletion skills/quest-log/assets/profiles/github.sh
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,8 @@ profile_link_blocks() {
# Already linked: return without writing. Skipping the write makes the
# operation idempotent in the common case and keeps it out of the
# read-modify-write race entirely.
if printf '%s\n' "$body" | rg -q "^Blocked by #$blocker\r?\$"; then
if printf '%s\n' "$body" |
rg -q --no-config "^Blocked by #$blocker( — [^[:space:]].*)?\r?\$"; then
printf '{}\n'
return 0
fi
Expand Down
5 changes: 3 additions & 2 deletions skills/return-to-town/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ After verifying the merged issue is closed, run the `quest-log` skill's canonica
recipe in Bash: `bash "$CLAUDE_PLUGIN_ROOT/skills/quest-log/assets/cleared-dependencies.sh" apply <owner/name>`. This is the primary
owner of the cleared-dependency `status:blocked → status:ready` edge. Report every readied
dependent and every retained dependent with its actionable reason. Do not limit the scan to
the merged issue's prose or comments: the recipe exhaustively evaluates canonical whole-line
`Blocked by #N` records on all open blocked, non-epic issues. A per-dependent failure does
the merged issue's prose or comments: the recipe exhaustively evaluates canonical
`Blocked by #N` records, including records with an exact ` — ` explanation suffix, on all
open blocked, non-epic issues. A per-dependent failure does
not prevent other dependents from being evaluated.

## After a merge (yours or the user's)
Expand Down
5 changes: 4 additions & 1 deletion skills/saga/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ the challenge summary.
numbers are already known — so no crash window exists before their links land.
2. **One topological pass over all entries** — adopted and created alike, blockers
before dependents, so every `Blocked by #<n>` resolves to an already-numbered
sibling. Per entry, in graph order:
sibling. The accepted dependency-record forms are the bare `Blocked by #<n>` and
`Blocked by #<n> — <non-empty explanation>` forms; prefer the bare form, use the exact
delimiter for an explanation, and put each blocker on its own line with no leading
whitespace. Per entry, in graph order:
- **Adopted entry:** link via the `sub_issues` API. If it sits in a dependent
position, apply `status:blocked` + append a `Blocked by #<n>` line (the stated,
confirmed exception to leave-untouched) — its blocker was handled earlier in the
Expand Down
7 changes: 4 additions & 3 deletions skills/seek-quest/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ Input: an optional caller-supplied risk allowlist and/or effort allowlist
- For each surviving candidate, scan its `body` line by line for the
`quest-log` dependency contract's three states:
- **No line begins `Blocked by #`.** This check passes.
- **At least one line is a canonical whole-line `Blocked by #M`
record** (case-sensitive, no leading/trailing content, decimal
digits after `#`). Resolve every referenced `M` with
- **At least one line contains a canonical `Blocked by #M` record.** The record is
case-sensitive, starts at the beginning of the line, contains decimal digits after
`#`, and either ends after `M` or carries non-empty prose after the exact ` — `
delimiter. Resolve every referenced `M` with
`gh issue view M --repo <owner/name> --json state`. Drop the
candidate unless every canonical reference resolves closed.
- **A line begins exactly `Blocked by #` but fails that grammar.**
Expand Down
6 changes: 4 additions & 2 deletions skills/sort-board/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ it, so no swap arises there.
what the issue text actually states.

Evaluate a sweep's blocked candidates under the `quest-log` canonical
cleared-dependency contract. Consider only whole-line `Blocked by #N` records in the
issue body, never comments, and resolve every distinct referenced issue with
cleared-dependency contract. Consider `Blocked by #N` records in the issue body, never
comments: a record starts at the beginning of a line and either ends after the decimal
issue number or carries non-empty prose after the exact ` — ` delimiter. Resolve every
distinct referenced issue with
`gh issue view <n> --repo <owner/name> --json state`. An open, missing, or unreadable
blocker, a malformed `Blocked by #` record, or a body with no canonical references
retains `status:blocked`; report the reason and propose no status swap. Once at least one
Expand Down
23 changes: 21 additions & 2 deletions tests/fixtures/quest-log/cleared-dependencies-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ gh() {
printf 'API \033[31mdenied\n' >&2
return 1
fi
printf '%s\n' '[[{"number":101,"state":"open","body":"Blocked by #1","labels":[{"name":"status:blocked"},{"name":"status:in-progress"}]},{"number":102,"state":"open","body":"Blocked by #1\nBlocked by #2","labels":[{"name":"status:blocked"}]},{"number":103,"state":"open","body":"Blocked by #abc","labels":[{"name":"status:blocked"}]},{"number":104,"state":"open","body":"Blocked by #1","labels":[{"name":"status:blocked"},{"name":"epic"}]}],[{"number":106,"state":"open","body":"Blocked by #1","labels":[{"name":"status:blocked"}]}]]'
printf '%s\n' '[[{"number":101,"state":"open","body":"Blocked by #1","labels":[{"name":"status:blocked"},{"name":"status:in-progress"}]},{"number":102,"state":"open","body":"Blocked by #1\nBlocked by #2","labels":[{"name":"status:blocked"}]},{"number":103,"state":"open","body":"Blocked by #abc","labels":[{"name":"status:blocked"}]},{"number":104,"state":"open","body":"Blocked by #1","labels":[{"name":"status:blocked"},{"name":"epic"}]}],[{"number":106,"state":"open","body":"Blocked by #1 — prerequisite lands first","labels":[{"name":"status:blocked"}]}]]'
return
fi
if [[ $1 == label && $2 == create ]]; then
Expand Down Expand Up @@ -116,14 +116,33 @@ reset_cleared_dependency_cache
cleared_dependency_body_verdict owner/repo 10 $'Blocked by #1\nBlocked by #1' ||
fail 'multiple closed canonical blockers should clear'
[[ $(wc -l <"$blocker_log") -eq 1 ]] || fail 'duplicate blockers were not deduplicated'
reset_cleared_dependency_cache
: >"$blocker_log"
cleared_dependency_body_verdict owner/repo 10 \
'Blocked by #1 — required registration point lands first' ||
fail 'an annotated closed blocker should clear'
[[ $(cat "$blocker_log") == 1 ]] || fail 'annotated record did not resolve its blocker id'
if cleared_dependency_body_verdict owner/repo 10 $'Blocked by #1\nBlocked by #2'; then
fail 'an open blocker must retain the dependent'
fi
# Assigned by the sourced canonical recipe.
# shellcheck disable=SC2154
[[ $cleared_dependency_reason == 'open blocker #2 retains #10' ]] ||
fail 'open-blocker report is not actionable'
for fixture in 'Blocked by #404' 'Blocked by #500' 'Blocked by #abc' ' Blocked by #1'; do
reset_cleared_dependency_cache
if cleared_dependency_body_verdict owner/repo 10 'Blocked by #2 — prerequisite is pending'; then
fail 'an annotated open blocker must retain the dependent'
fi
[[ $cleared_dependency_reason == 'open blocker #2 retains #10' ]] ||
fail 'annotated open-blocker report is not actionable'
for fixture in \
'Blocked by #404' \
'Blocked by #500' \
'Blocked by #abc' \
' Blocked by #1' \
'Blocked by #1 explanation without delimiter' \
'Blocked by #1 —' \
'Blocked by #1 — explanation after two spaces'; do
if cleared_dependency_body_verdict owner/repo 10 "$fixture"; then
fail "invalid dependency record cleared: $fixture"
fi
Expand Down
18 changes: 17 additions & 1 deletion tests/fixtures/quest-log/tracker-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,13 @@ fi
if [[ $1 == issue && $2 == view ]]; then
case " $* " in
*" comments "*) printf '%s\n' '{"comments":[{"body":"first"},{"body":"second"}]}' ;;
*" body "*) printf 'existing body\r\nBlocked by #7\r\n' ;;
*" body "*)
if [[ ${GH_ANNOTATED_BODY:-false} == true ]]; then
printf 'existing body\r\nBlocked by #7 — prerequisite lands first\r\n'
else
printf 'existing body\r\nBlocked by #7\r\n'
fi
;;
*) printf '%s\n' '{"number":101,"title":"T","body":"B","labels":[],"parent":null,"state":"OPEN","url":"u","updatedAt":"2026-01-01T00:00:00Z"}' ;;
esac
exit 0
Expand Down Expand Up @@ -771,6 +777,16 @@ edits=$(rg -c '^issue edit ' "$sandbox/calls" || true)
[[ ${edits:-0} == 0 ]] ||
fail 'link-blocks rewrote a body that already carried the link (CRLF guard)'

# An annotated dependency record is the same link and must also be idempotent.
: >"$sandbox/calls"
GH_ANNOTATED_BODY=true GH_CALL_LOG="$sandbox/calls" PATH="$sandbox/bin:$PATH" \
"$tracker" link-blocks --profile github --target example/repo 7 101 \
>"$sandbox/out" 2>"$sandbox/err" ||
fail 'link-blocks rejected an annotated existing link'
edits=$(rg -c '^issue edit ' "$sandbox/calls" || true)
[[ ${edits:-0} == 0 ]] ||
fail 'link-blocks duplicated an annotated existing link'

# view now carries a real updated timestamp rather than a permanent null.
run_op view-updated -- view --profile github --target example/repo 101
jq -e '.updated == "2026-01-01T00:00:00Z"' >/dev/null <"$sandbox/out" ||
Expand Down