From 323d326dfac4b84d8f85a6384a4df012a2cc78e2 Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:07:00 -0400 Subject: [PATCH 01/10] fix(worklog): validate canonical task layout --- skills/worklog/bin/_lint.py | 60 +++++++++++++++++++- skills/worklog/tests/lint/test_file_scope.sh | 43 +++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/skills/worklog/bin/_lint.py b/skills/worklog/bin/_lint.py index f3f1a9a..5d4223d 100755 --- a/skills/worklog/bin/_lint.py +++ b/skills/worklog/bin/_lint.py @@ -13,6 +13,8 @@ - `repos` is a list (or absent). - Relations (`parent_slug`, `related[].slug`, `supersedes`, `superseded_by`, `reopens`) resolve to real task files under people/*/{active,archive}/. + - Task Markdown lives only under people/*/{active,archive}/ and slugs are + globally unique across task-bearing state directories. - `related[]` entries have a `note`. - State/status consistency: files under archive/ should have `status: archived` (warn if not). @@ -78,6 +80,59 @@ def _collect(root: pathlib.Path) -> list[tuple[pathlib.Path, str]]: return out +def _task_slug(path: pathlib.Path) -> str: + text = path.read_text() + match = FRONTMATTER_RE.match(text) + if match: + try: + frontmatter = yaml.safe_load(match.group(1)) or {} + except yaml.YAMLError: + frontmatter = {} + if isinstance(frontmatter, dict) and frontmatter.get("slug"): + return str(frontmatter["slug"]) + return path.stem + + +def _layout_issues(root: pathlib.Path) -> list[dict[str, Any]]: + people = root / "people" + issues: list[dict[str, Any]] = [] + task_paths: list[pathlib.Path] = [] + + for ldap_dir in sorted(people.glob("*")): + if not ldap_dir.is_dir(): + continue + for state_dir in sorted(path for path in ldap_dir.iterdir() if path.is_dir()): + markdown = sorted(state_dir.rglob("*.md")) + task_paths.extend(markdown) + if state_dir.name not in {"active", "archive"} and markdown: + rel = state_dir.relative_to(root) + issues.append({ + "file": str(rel), + "state": "layout", + "errors": [ + f"unknown task state directory '{rel}' contains {len(markdown)} " + "Markdown task file(s); supported states: active, archive" + ], + "warnings": [], + }) + + paths_by_slug: dict[str, list[pathlib.Path]] = {} + for path in task_paths: + paths_by_slug.setdefault(_task_slug(path), []).append(path) + for slug, paths in sorted(paths_by_slug.items()): + if len(paths) < 2: + continue + rendered = ", ".join(str(path.relative_to(root)) for path in paths) + issues.append({ + "file": "people", + "state": "layout", + "errors": [f"duplicate slug '{slug}' appears in: {rendered}"], + "warnings": [], + }) + + return issues + + def _strict_yaml(raw: str) -> tuple[dict[str, Any] | None, str | None]: try: fm = yaml.safe_load(raw) @@ -583,6 +638,7 @@ def main() -> None: sys.exit(2) else: files = all_files + layout_report = [] if single_file else _layout_issues(root) known_slugs: set[str] = set() for path, _ in all_files: text = path.read_text() @@ -621,8 +677,8 @@ def main() -> None: if missing and _apply_fix_related(path, missing): fixed_files.append((str(path.relative_to(root)), missing)) - report: list[dict[str, Any]] = [] - total_errors = 0 + report: list[dict[str, Any]] = list(layout_report) + total_errors = sum(len(issue["errors"]) for issue in layout_report) total_warnings = 0 for path, state in files: errors, warnings = _lint_file( diff --git a/skills/worklog/tests/lint/test_file_scope.sh b/skills/worklog/tests/lint/test_file_scope.sh index 79bf88d..413219a 100755 --- a/skills/worklog/tests/lint/test_file_scope.sh +++ b/skills/worklog/tests/lint/test_file_scope.sh @@ -80,4 +80,45 @@ if "$WORKLOG_BIN/lint.sh" --file=people/tester/active/missing-task.md >/tmp/work fi grep -q 'is not a tracked task file' /tmp/worklog-lint-missing.out -echo "ok: lint --file scope" +mkdir -p people/tester/archived +cat > people/tester/archived/good-task.md <<'EOF' +--- +slug: good-task +kind: impl +status: archived +project: sample +last_updated: 2026-06-12 +next_action: — +repos: [sample] +--- + +## Context +Invisible duplicate in an unsupported state directory. + +## Next +None. +EOF + +layout_json="$("$WORKLOG_BIN/lint.sh" --format=json 2>/dev/null || true)" +printf '%s' "$layout_json" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +errors = [ + error + for issue in d["issues"] + for error in issue["errors"] +] +assert any("unknown task state directory" in error and "archived" in error for error in errors), errors +assert any("duplicate slug" in error and "good-task" in error for error in errors), errors +' + +# File-scoped lint remains scoped to the requested canonical task. +good_json="$("$WORKLOG_BIN/lint.sh" --file=people/tester/active/good-task.md --format=json)" +printf '%s' "$good_json" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +assert d["total_files"] == 1, d +assert d["total_errors"] == 0, d +' + +echo "ok: lint --file scope and vault layout" From 649b0140d65ca7efe61865369effc792a33b209c Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:08:46 -0400 Subject: [PATCH 02/10] fix(worklog): keep light init non-mutating --- skills/worklog/SKILL.md | 4 +- skills/worklog/modes/init.md | 5 ++ skills/worklog/tests/init/test_light_init.sh | 59 ++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100755 skills/worklog/tests/init/test_light_init.sh diff --git a/skills/worklog/SKILL.md b/skills/worklog/SKILL.md index 55aa22d..b54c5c3 100644 --- a/skills/worklog/SKILL.md +++ b/skills/worklog/SKILL.md @@ -37,7 +37,7 @@ Once a known mode is parsed: run preamble (per table), read `modes/.md`, f | Mode | Preamble | `references/protocol.md` | Reads AGENTS.md? | lessons.md? | |---------|----------|--------------------------|------------------|-------------| -| init | `--full` | no | yes | quickref (limit=15) | +| init | default / `--light` → `--minimal`; explicit `--full` → `--full` | no | yes | quickref (limit=15) | | sync | `--full` | only when creating or hand-editing a task | only for an edge case the reference does not answer | no | | status | `--minimal` | no | no | no | | context | `--minimal` | no | no | no | @@ -76,7 +76,7 @@ Run helpers from a shell that has the target clone's environment loaded. Prefer cd "$WORKLOG_REPO" && "$WORKLOG_BIN/preamble.sh" [--minimal|--full] ``` -Emits `LDAP=`, `PROJECTS_DIR=`, `NAMESPACE=`, `PULL=` key/value lines plus a `### roster` block (top 15 active tasks by `last_updated`, one tab-separated line each). Internally handles: LDAP resolve (24h cached), namespace bootstrap, rate-limited `git pull` (5-min stamp), `.gitconfig.lock` cleanup, autosave-if-dirty. +Emits `LDAP=`, `PROJECTS_DIR=`, `NAMESPACE=`, `PULL=` key/value lines plus a `### roster` block (top 15 active tasks by `last_updated`, one tab-separated line each). Both paths resolve LDAP and inspect the namespace/roster. Full mode additionally handles the rate-limited pull and dirty-tree autosave. Skip re-invocation within the same session — preamble.sh is idempotent but the tool turns aren't free. diff --git a/skills/worklog/modes/init.md b/skills/worklog/modes/init.md index 900e07f..3868d11 100644 --- a/skills/worklog/modes/init.md +++ b/skills/worklog/modes/init.md @@ -1,5 +1,10 @@ # Mode: `init` +**Routing contract:** Default and `--light` use `preamble.sh --minimal`; only +explicit `--full` uses `preamble.sh --full`. Select the preamble from the user +flag before reading this mode. Light init must not pull, autosave, commit, push, +or rebuild caches. + Onboard a session. Light by default; escalates to a full external scan when drift is detected or the user explicitly asks. ## Detection — light vs. full diff --git a/skills/worklog/tests/init/test_light_init.sh b/skills/worklog/tests/init/test_light_init.sh new file mode 100755 index 0000000..a5dff7f --- /dev/null +++ b/skills/worklog/tests/init/test_light_init.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +WORKLOG_BIN="$ROOT/bin" +SKILL_ROOT="$ROOT/SKILL.md" +INIT_MODE="$ROOT/modes/init.md" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +grep -Fq 'default / `--light` → `--minimal`' "$SKILL_ROOT" +grep -Fq 'explicit `--full` → `--full`' "$SKILL_ROOT" +grep -Fq 'Default and `--light` use `preamble.sh --minimal`' "$INIT_MODE" + +mkdir -p "$TMP/people/tester/active" "$TMP/people/tester/archive" "$TMP/.cache" +git -C "$TMP" init -q +git -C "$TMP" config user.email tester@example.com +git -C "$TMP" config user.name Tester + +cat > "$TMP/people/tester/active/light-init.md" <<'EOF' +--- +slug: light-init +kind: investigation +status: in-progress +project: sample +last_updated: 2026-07-25 +next_action: Verify light init +repos: [sample] +--- + +## Context +Dirty-clone light-init fixture. + +## Next +Verify light init. +EOF + +printf '[]\n' > "$TMP/.cache/compact-kernels.json" +printf 'keep\n' > "$TMP/.cache/preamble-pull-stamp" +git -C "$TMP" add people .cache +git -C "$TMP" commit -qm "fixture" +printf '\ndirty\n' >> "$TMP/people/tester/active/light-init.md" + +before_status="$(git -C "$TMP" status --porcelain)" +before_kernel_mtime="$(stat -f %m "$TMP/.cache/compact-kernels.json" 2>/dev/null || stat -c %Y "$TMP/.cache/compact-kernels.json")" +before_pull_mtime="$(stat -f %m "$TMP/.cache/preamble-pull-stamp" 2>/dev/null || stat -c %Y "$TMP/.cache/preamble-pull-stamp")" + +WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/preamble.sh" --minimal >/dev/null + +after_status="$(git -C "$TMP" status --porcelain)" +after_kernel_mtime="$(stat -f %m "$TMP/.cache/compact-kernels.json" 2>/dev/null || stat -c %Y "$TMP/.cache/compact-kernels.json")" +after_pull_mtime="$(stat -f %m "$TMP/.cache/preamble-pull-stamp" 2>/dev/null || stat -c %Y "$TMP/.cache/preamble-pull-stamp")" + +[[ "$after_status" == "$before_status" ]] +[[ "$after_kernel_mtime" == "$before_kernel_mtime" ]] +[[ "$after_pull_mtime" == "$before_pull_mtime" ]] + +echo "ok: default/light init routes to non-mutating minimal preamble" From 29431d05540bf784d83a4ceca158c2420183dd26 Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:09:40 -0400 Subject: [PATCH 03/10] fix(worklog): hydrate only the focused tracker --- skills/worklog/modes/init.md | 21 +++++++++++++------- skills/worklog/tests/init/test_light_init.sh | 5 +++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/skills/worklog/modes/init.md b/skills/worklog/modes/init.md index 3868d11..69df273 100644 --- a/skills/worklog/modes/init.md +++ b/skills/worklog/modes/init.md @@ -53,17 +53,24 @@ drift: ready — which task? ``` -## Tracker hydration (MUST, on every init) +## Tracker hydration (after focus selection) -After printing the active-task list and before asking "which task?", **hydrate the in-session tracker** for any active task with ≥3 unchecked items in its `## Next` section. Per AGENTS.md § In-session progress visibility: +The active-task list is orientation, not an instruction to materialize every +durable task in an ephemeral tracker. Do not hydrate `TaskCreate` or +`update_plan` before the user selects a task. -- **Claude Code:** invoke `TaskCreate` for each unchecked `- [ ]` item under `## Next`. Use the slug as the task's `metadata.slug` so the tracker entry maps back to its source task file. -- **OpenAI Codex CLI:** emit an initial `update_plan` populated from the same `## Next` items. -- **Cursor:** populate the canvas todo card / Plan Mode entries. +After the user selects or resumes a slug, run +`"$WORKLOG_BIN/context.sh" `. Its "Tracker-ready snippet" formats that +task's unchecked `## Next` items. Hydrate only that focused task: -Run `"$WORKLOG_BIN/context.sh" ` for any focused task — its output's "Tracker-ready snippet" section formats each unchecked item ready to paste/exec. +- **Claude Code:** invoke `TaskCreate` for its unchecked items, using the slug + as `metadata.slug`. +- **OpenAI Codex CLI:** emit `update_plan` for the same focused items. +- **Cursor:** populate the focused canvas todo card / Plan Mode entries. -Skip hydration only when every active task is at ≤2 unchecked items (single-step or trivial). Don't wait for the user to ask. Drift evidence: 2026-04-27 review session ran ~30 multi-step commits with zero `TaskCreate` invocations despite the system reminder firing repeatedly (`docs/lessons.md` 2026-04 entry). +Skip hydration when the selected task has at most two unchecked items. The +durable task files remain the multi-task backlog; the tracker mirrors only the +work this session intends to execute. ## Full path diff --git a/skills/worklog/tests/init/test_light_init.sh b/skills/worklog/tests/init/test_light_init.sh index a5dff7f..33ef9fb 100755 --- a/skills/worklog/tests/init/test_light_init.sh +++ b/skills/worklog/tests/init/test_light_init.sh @@ -11,6 +11,11 @@ trap 'rm -rf "$TMP"' EXIT grep -Fq 'default / `--light` → `--minimal`' "$SKILL_ROOT" grep -Fq 'explicit `--full` → `--full`' "$SKILL_ROOT" grep -Fq 'Default and `--light` use `preamble.sh --minimal`' "$INIT_MODE" +grep -Fq 'Do not hydrate `TaskCreate` or `update_plan` before the user selects a task.' "$INIT_MODE" +if grep -Fq 'for any active task with ≥3 unchecked items' "$INIT_MODE"; then + echo "FAIL: init still hydrates every multi-step active task" + exit 1 +fi mkdir -p "$TMP/people/tester/active" "$TMP/people/tester/archive" "$TMP/.cache" git -C "$TMP" init -q From 75dc60bed77dc1497692ce3dc84db4a6f9330cec Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:11:46 -0400 Subject: [PATCH 04/10] feat(worklog): centralize public mode registry --- skills/worklog/bin/codex-surface-check.sh | 22 +++++++++++- skills/worklog/modes/registry.md | 21 +++++++++++ .../tests/surface/test_mode_registry.sh | 36 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 skills/worklog/modes/registry.md create mode 100755 skills/worklog/tests/surface/test_mode_registry.sh diff --git a/skills/worklog/bin/codex-surface-check.sh b/skills/worklog/bin/codex-surface-check.sh index 43782bf..58d059f 100755 --- a/skills/worklog/bin/codex-surface-check.sh +++ b/skills/worklog/bin/codex-surface-check.sh @@ -15,9 +15,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(resolve_worklog_repo)" || exit 1 cd "$REPO_ROOT" -EXPECTED=(help init sync status context plan spawn export import lint project scrape-slack review) CODEX_SKILL_PATH="${CODEX_SKILL_PATH:-$HOME/.codex/skills/worklog/SKILL.md}" MODE_INIT_PATH="${MODE_INIT_PATH:-$SCRIPT_DIR/../modes/init.md}" +MODE_REGISTRY_PATH="${MODE_REGISTRY_PATH:-$SCRIPT_DIR/../modes/registry.md}" case "${1:-}" in -h|--help) @@ -29,6 +29,8 @@ the same worklog command names. Environment: CODEX_SKILL_PATH=/path/to/SKILL.md override local Codex skill path + MODE_REGISTRY_PATH=/path/to/registry.md + override public mode registry EOF exit 0 ;; @@ -40,6 +42,24 @@ EOF ;; esac +if [[ ! -f "$MODE_REGISTRY_PATH" ]]; then + echo "codex-surface-check: mode registry not found at $MODE_REGISTRY_PATH" >&2 + exit 1 +fi + +EXPECTED=() +while IFS= read -r cmd; do + [[ -n "$cmd" ]] && EXPECTED+=("$cmd") +done < <( + sed -n '/MODE_REGISTRY_BEGIN/,/MODE_REGISTRY_END/ { + s/^- \([a-z0-9-][a-z0-9-]*\)$/\1/p + }' "$MODE_REGISTRY_PATH" +) +if [[ ${#EXPECTED[@]} -eq 0 ]]; then + echo "codex-surface-check: mode registry is empty or malformed" >&2 + exit 1 +fi + SURFACES=("README.md" "AGENTS.md") if [[ -f "$CODEX_SKILL_PATH" ]]; then SURFACES+=("$CODEX_SKILL_PATH") diff --git a/skills/worklog/modes/registry.md b/skills/worklog/modes/registry.md new file mode 100644 index 0000000..205ef80 --- /dev/null +++ b/skills/worklog/modes/registry.md @@ -0,0 +1,21 @@ +# Public mode registry + +One public command per line, in menu order. `codex-surface-check.sh` consumes +the marked block; keep behavior and prose in `SKILL.md` and the selected mode +file. + + +- help +- init +- sync +- status +- context +- plan +- spawn +- export +- import +- lint +- project +- scrape-slack +- review + diff --git a/skills/worklog/tests/surface/test_mode_registry.sh b/skills/worklog/tests/surface/test_mode_registry.sh new file mode 100755 index 0000000..5e65f9a --- /dev/null +++ b/skills/worklog/tests/surface/test_mode_registry.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +WORKLOG_BIN="$ROOT/bin" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +mkdir -p "$TMP/repo/people/tester/active" "$TMP/repo/people/tester/archive" +cp "$ROOT/templates/README.md" "$TMP/repo/README.md" +cp "$ROOT/templates/AGENTS.md" "$TMP/repo/AGENTS.md" +git -C "$TMP/repo" init -q + +WORKLOG_REPO="$TMP/repo" \ +WORKLOG_LDAP=tester \ +CODEX_SKILL_PATH="$ROOT/SKILL.md" \ +MODE_REGISTRY_PATH="$ROOT/modes/registry.md" \ +MODE_INIT_PATH="$ROOT/modes/init.md" \ + "$WORKLOG_BIN/codex-surface-check.sh" >/dev/null + +awk ' + /MODE_REGISTRY_END/ { print "- fixture-mode" } + { print } +' "$ROOT/modes/registry.md" > "$TMP/registry.md" + +if WORKLOG_REPO="$TMP/repo" \ + WORKLOG_LDAP=tester \ + CODEX_SKILL_PATH="$ROOT/SKILL.md" \ + MODE_REGISTRY_PATH="$TMP/registry.md" \ + MODE_INIT_PATH="$ROOT/modes/init.md" \ + "$WORKLOG_BIN/codex-surface-check.sh" >/dev/null 2>&1; then + echo "FAIL: checker ignored a registry-only mode" + exit 1 +fi + +echo "ok: command surfaces follow the packaged mode registry" From f9f55594e44df2f37ff6e192b22fb409369e73a2 Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:14:00 -0400 Subject: [PATCH 05/10] test(worklog): replay stale derived views --- .../tests/cache/test_stale_consumers.sh | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100755 skills/worklog/tests/cache/test_stale_consumers.sh diff --git a/skills/worklog/tests/cache/test_stale_consumers.sh b/skills/worklog/tests/cache/test_stale_consumers.sh new file mode 100755 index 0000000..e1b1363 --- /dev/null +++ b/skills/worklog/tests/cache/test_stale_consumers.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +WORKLOG_BIN="$ROOT/bin" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +mkdir -p "$TMP/people/tester/active" "$TMP/people/tester/archive" +git -C "$TMP" init -q +git -C "$TMP" config user.email tester@example.com +git -C "$TMP" config user.name Tester + +cat > "$TMP/people/tester/active/cache-task.md" <<'EOF' +--- +slug: cache-task +kind: investigation +status: draft +project: sample +last_updated: 2026-07-25 +next_action: Read the original task state +repos: [sample] +--- + +## Context +Derived-view invalidation fixture. + +## Next +- [ ] Read the original task state +EOF + +WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/search.sh" --list --status=draft \ + | grep -Eq '^active +draft +cache-task$' +[[ -s "$TMP/.cache/index.jsonl" ]] + +sleep 1 +python3 - "$TMP/people/tester/active/cache-task.md" <<'PY' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +text = path.read_text() +text = text.replace("status: draft", "status: in-progress") +text = text.replace( + "Read the original task state", + "Use the updated raw task state", +) +path.write_text(text) +PY + +WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/search.sh" --list --status=in-progress \ + | grep -Eq '^active +in-progress +cache-task$' +if WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/search.sh" --list --status=draft 2>/dev/null | grep -q 'cache-task'; then + echo "FAIL: stale index returned the old task status" + exit 1 +fi + +WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/compact-kernels.sh" >/dev/null +touch -t 202001010000 \ + "$TMP/.cache/compact-kernels.md" \ + "$TMP/.cache/compact-kernels.json" + +preamble="$( + WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/preamble.sh" --minimal +)" +grep -q 'roster-health: stale' <<< "$preamble" +if grep -q 'Use the updated raw task state' <<< "$preamble"; then + echo "FAIL: stale kernel content leaked into the preamble roster" + exit 1 +fi + +context="$( + WORKLOG_REPO="$TMP" WORKLOG_LDAP=tester \ + "$WORKLOG_BIN/context.sh" cache-task +)" +grep -q 'Use the updated raw task state' <<< "$context" + +echo "ok: stale index rebuilds and stale kernels fall through to raw context" From 1cfaf03ff5910ff96d5ddc582a89f4ed1a6a036e Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:15:10 -0400 Subject: [PATCH 06/10] test(worklog): stabilize tracker scope contract --- skills/worklog/modes/init.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skills/worklog/modes/init.md b/skills/worklog/modes/init.md index 69df273..f6a007d 100644 --- a/skills/worklog/modes/init.md +++ b/skills/worklog/modes/init.md @@ -56,8 +56,9 @@ ready — which task? ## Tracker hydration (after focus selection) The active-task list is orientation, not an instruction to materialize every -durable task in an ephemeral tracker. Do not hydrate `TaskCreate` or -`update_plan` before the user selects a task. +durable task in an ephemeral tracker. + +Do not hydrate `TaskCreate` or `update_plan` before the user selects a task. After the user selects or resumes a slug, run `"$WORKLOG_BIN/context.sh" `. Its "Tracker-ready snippet" formats that From 03277968e76da4710a92cbac2f60adfce0e1c526 Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:35:09 -0400 Subject: [PATCH 07/10] fix(worklog): preserve transcript layout Exclude supported transcript Markdown from task-state and duplicate-slug validation while retaining checks for genuinely misplaced task files. Worklog-Slug: worklog-protocol-efficiency --- skills/worklog/bin/_lint.py | 7 ++++++- skills/worklog/tests/lint/test_file_scope.sh | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/skills/worklog/bin/_lint.py b/skills/worklog/bin/_lint.py index 5d4223d..c6cbbbf 100755 --- a/skills/worklog/bin/_lint.py +++ b/skills/worklog/bin/_lint.py @@ -102,9 +102,14 @@ def _layout_issues(root: pathlib.Path) -> list[dict[str, Any]]: if not ldap_dir.is_dir(): continue for state_dir in sorted(path for path in ldap_dir.iterdir() if path.is_dir()): + if state_dir.name in {"active", "archive"}: + task_paths.extend(sorted(state_dir.glob("*.md"))) + continue + if state_dir.name == "transcripts": + continue markdown = sorted(state_dir.rglob("*.md")) task_paths.extend(markdown) - if state_dir.name not in {"active", "archive"} and markdown: + if markdown: rel = state_dir.relative_to(root) issues.append({ "file": str(rel), diff --git a/skills/worklog/tests/lint/test_file_scope.sh b/skills/worklog/tests/lint/test_file_scope.sh index 413219a..0546b59 100755 --- a/skills/worklog/tests/lint/test_file_scope.sh +++ b/skills/worklog/tests/lint/test_file_scope.sh @@ -99,6 +99,14 @@ Invisible duplicate in an unsupported state directory. None. EOF +# Transcripts are supported auxiliary Markdown and intentionally share task slugs. +mkdir -p people/tester/transcripts +cat > people/tester/transcripts/good-task.md <<'EOF' +# Transcript + +Session evidence for good-task. +EOF + layout_json="$("$WORKLOG_BIN/lint.sh" --format=json 2>/dev/null || true)" printf '%s' "$layout_json" | python3 -c ' import json, sys @@ -110,6 +118,7 @@ errors = [ ] assert any("unknown task state directory" in error and "archived" in error for error in errors), errors assert any("duplicate slug" in error and "good-task" in error for error in errors), errors +assert not any("transcripts" in error for error in errors), errors ' # File-scoped lint remains scoped to the requested canonical task. From 5945d7020fd23689414db20052caae9176ec24f1 Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:39:59 -0400 Subject: [PATCH 08/10] style(worklog): satisfy Ruff 0.16 import layout Normalize the pre-existing import boundary so the CI job's unpinned Ruff 0.16 release agrees with the locally installed Ruff 0.15 formatter. Worklog-Slug: worklog-protocol-efficiency --- skills/worklog/bin/_boundary_lint.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skills/worklog/bin/_boundary_lint.py b/skills/worklog/bin/_boundary_lint.py index 1ce0ad0..33636cd 100755 --- a/skills/worklog/bin/_boundary_lint.py +++ b/skills/worklog/bin/_boundary_lint.py @@ -13,7 +13,6 @@ from dataclasses import dataclass from typing import Any - DEFAULT_INCLUDES = [ "people/*/active/*.md", "people/*/archive/*.md", From 39abbfe056a1db93d2705c580abd216565b52d33 Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:48:35 -0400 Subject: [PATCH 09/10] ci(worklog): pin Ruff lint contract Keep the Worklog CI gate on the repository's established Ruff 0.15.12 rule set instead of absorbing a surprise 0.16 migration into this protocol PR. Restore the incidental formatter-only delta from the failed upgrade attempt. Worklog-Slug: worklog-protocol-efficiency --- .github/workflows/ci.yml | 2 +- skills/worklog/bin/_boundary_lint.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a2738a..3877cf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: run: | sudo apt-get update -qq sudo apt-get install -y shellcheck jq - pip install --quiet ruff pyyaml + pip install --quiet 'ruff==0.15.12' pyyaml - name: set git identity for fixture vault commits run: | git config --global user.email "ci@cheshirecode.test" diff --git a/skills/worklog/bin/_boundary_lint.py b/skills/worklog/bin/_boundary_lint.py index 33636cd..1ce0ad0 100755 --- a/skills/worklog/bin/_boundary_lint.py +++ b/skills/worklog/bin/_boundary_lint.py @@ -13,6 +13,7 @@ from dataclasses import dataclass from typing import Any + DEFAULT_INCLUDES = [ "people/*/active/*.md", "people/*/archive/*.md", From 655f9a3e5d71e59c1428bcf27a64c5ed7090e7dc Mon Sep 17 00:00:00 2001 From: cheshirecode Date: Sat, 25 Jul 2026 23:51:54 -0400 Subject: [PATCH 10/10] ci(worklog): report pinned Ruff version Print the resolved Ruff version immediately before the Worklog suite so future rule-set drift is visible in the failing job without log archaeology. Worklog-Slug: worklog-protocol-efficiency --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3877cf8..2e14993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,4 +73,6 @@ jobs: git config --global user.name "ci-fixture" git config --global init.defaultBranch main - name: run worklog-skill test mode - run: bash tests/run.sh worklog-skill + run: | + ruff --version + bash tests/run.sh worklog-skill