From de366be6e7910ce4f8437d733948a6bbe6d41f7e Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Mon, 29 Jun 2026 11:32:54 -0400 Subject: [PATCH] Fix worklog context lookup hydration --- skills/worklog/bin/_context.py | 27 ++++++- skills/worklog/bin/context.sh | 16 +++- skills/worklog/bin/okf.py | 1 - skills/worklog/tests/context/test_context.sh | 85 ++++++++++++++++++++ tests/run.sh | 6 ++ 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100755 skills/worklog/tests/context/test_context.sh diff --git a/skills/worklog/bin/_context.py b/skills/worklog/bin/_context.py index 491fc37..37fdc70 100755 --- a/skills/worklog/bin/_context.py +++ b/skills/worklog/bin/_context.py @@ -34,6 +34,31 @@ def parse_task_file(text: str) -> tuple[dict, str]: return fm if isinstance(fm, dict) else {}, match.group(2) +def next_section(body: str) -> str: + """Return the current top-level `## Next` section body. + + Older checkpoint notes can contain historical `## Next` headings and + checkboxes. Tracker hydration should mirror only the durable current plan, + not every stale checklist ever written into the task body. + """ + lines = body.splitlines() + collected = [] + in_next = False + + for line in lines: + if re.match(r"^##\s+Next\b", line): + if in_next: + break + in_next = True + continue + if in_next and re.match(r"^##\s+", line): + break + if in_next: + collected.append(line) + + return "\n".join(collected) + + def parse_work_items(body: str) -> list[dict]: """Parse `- [ ]` / `- [x]` checkboxes, folding soft-wrapped continuation lines into the same bullet. A continuation line is indented further than @@ -42,7 +67,7 @@ def parse_work_items(body: str) -> list[dict]: current = None bullet_re = re.compile(r"^(\s*)-\s*\[([ xX])\]\s+(.+?)\s*$") new_block_re = re.compile(r"^\s*(?:[-*#]|\d+\.)\s") - for line in body.splitlines(): + for line in next_section(body).splitlines(): m = bullet_re.match(line) if m: current = { diff --git a/skills/worklog/bin/context.sh b/skills/worklog/bin/context.sh index 7b18d51..61158a2 100755 --- a/skills/worklog/bin/context.sh +++ b/skills/worklog/bin/context.sh @@ -49,8 +49,20 @@ LDAP="$(resolve_ldap)" FILE="people/$LDAP/active/$SLUG.md" [[ -f "$FILE" ]] || FILE="people/$LDAP/archive/$SLUG.md" if [[ ! -f "$FILE" ]]; then - echo "context: $SLUG not found under people/$LDAP/{active,archive}/" >&2 - exit 1 + matches=() + while IFS= read -r match; do + matches+=("$match") + done < <(find people -path "*/active/$SLUG.md" -o -path "*/archive/$SLUG.md" | sort) + if [[ ${#matches[@]} -eq 1 ]]; then + FILE="${matches[0]}" + elif [[ ${#matches[@]} -gt 1 ]]; then + echo "context: $SLUG is ambiguous across namespaces:" >&2 + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + else + echo "context: $SLUG not found under people/*/{active,archive}/" >&2 + exit 1 + fi fi # Commit history for this slug (follows renames via Worklog-Previous-Slug). diff --git a/skills/worklog/bin/okf.py b/skills/worklog/bin/okf.py index abddb2c..b80b8cb 100755 --- a/skills/worklog/bin/okf.py +++ b/skills/worklog/bin/okf.py @@ -355,7 +355,6 @@ def cmd_doctor(args: argparse.Namespace) -> int: if local_noise: warnings.append(f"local/generated files are visible to git status: {', '.join(local_noise[:5])}") - dry_args = argparse.Namespace(repo=str(root), apply=False, check=False) pending: list[str] = [] for path in _tracked_markdown(root): updated, did_change = sync_source_text(root, path, _read(path)) diff --git a/skills/worklog/tests/context/test_context.sh b/skills/worklog/tests/context/test_context.sh new file mode 100755 index 0000000..7f6c499 --- /dev/null +++ b/skills/worklog/tests/context/test_context.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +WORKLOG_BIN="${WORKLOG_BIN:-$(cd "$(dirname "$0")/../../bin" && pwd)}" +SCRATCH_ROOT="$(mktemp -d -t worklog-context-test-XXXXXX)" +SCRATCH="$SCRATCH_ROOT/repo" +export TMPDIR="$SCRATCH_ROOT/tmp" +mkdir -p "$TMPDIR" +trap 'rm -rf "$SCRATCH_ROOT"' EXIT + +git init -q "$SCRATCH" +cd "$SCRATCH" +git config user.email "tester@example.com" +git config user.name "Tester" +export WORKLOG_REPO="$SCRATCH" +export WORKLOG_LDAP="tester" + +mkdir -p people/tester/active people/peer/active + +cat > people/tester/active/current-next.md <<'EOF' +--- +slug: current-next +kind: impl +status: in-progress +project: context-test +last_updated: 2026-06-29 +next_action: "Use only the current Next section" +repos: [context-test] +--- + +## Context + +This task has a historical checklist that should not hydrate the tracker. + +## Next + +- [ ] Current item one +- [ ] Current item two + +## Notes from older session + +## Next + +- [ ] Stale item from old notes +EOF + +cat > people/peer/active/peer-only.md <<'EOF' +--- +slug: peer-only +kind: impl +status: in-progress +project: context-test +last_updated: 2026-06-29 +next_action: "Read-only peer lookup" +repos: [context-test] +--- + +## Context + +Peer-owned task used to prove unique read-only slug lookup. + +## Next + +- [ ] Peer item +EOF + +git add people/ +git commit -q -m "context-test: seed tasks" --no-verify + +json="$(WORKLOG_LDAP=tester "$WORKLOG_BIN/context.sh" current-next --format=json)" +CONTEXT_JSON="$json" python3 - <<'PY' +import json +import os +import sys + +data = json.loads(os.environ["CONTEXT_JSON"]) +items = [item["text"] for item in data["work_items"] if item["status"] == "open"] +assert items == ["Current item one", "Current item two"], items +PY + +out="$(WORKLOG_LDAP=tester "$WORKLOG_BIN/context.sh" peer-only --for=compact)" +printf '%s\n' "$out" | grep -q '^slug: peer-only$' +printf '%s\n' "$out" | grep -q 'Read-only peer lookup' + +echo "ok: context current-next hydration and unique peer lookup" diff --git a/tests/run.sh b/tests/run.sh index 31ceae1..d50b2dc 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -359,6 +359,12 @@ test_worklog_skill() { fail "worklog-manager poll fixture" fi + if bash "$skill/tests/context/test_context.sh" >/dev/null 2>&1; then + ok "context current Next + unique slug fixture" + else + fail "context current Next + unique slug fixture" + fi + rm -rf "$(dirname "$vault")" }