From cdfdb1c02e11a921b2974ed0c208adc831cd9681 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 17:56:10 +0000 Subject: [PATCH 1/9] docs(work-items): document local-markdown branch and worktree confinement (#2944) Co-authored-by: Kyle Sexton --- plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 10 ++++ plugins/work-items/README.md | 7 ++- plugins/work-items/skills/setup/SKILL.md | 10 +++- .../tools/work-item-tracker/CONTRACT.md | 30 ++++++++++ .../adapters/local-markdown/README.md | 60 +++++++++++++++++++ 6 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 plugins/work-items/tools/work-item-tracker/adapters/local-markdown/README.md diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 4ca424213..a2044e68c 100644 --- a/plugins/work-items/.claude-plugin/plugin.json +++ b/plugins/work-items/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "work-items", - "version": "0.35.24", + "version": "0.35.25", "description": "Manages development work items through a provider-neutral tracker seam that ships with the plugin (bundled dispatcher plus github and local-markdown adapters; seam plugin-dir canonical, adapters consumer-local-first): dashboard, taxonomy-labeled creation, a race-safe assignee-plus-lease claim protocol, recurring-schedule checks, TODO scanning, stale-lease auditing, plan decomposition into vertical-slice items, raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states), plus the two work-items loop lanes of the loop-lane convention: a self-paced autonomous work-loop drain (work-class admission gate, adaptive item cap, PR-only) and an attended attend-queue escalation lane. The re-runnable setup skill binds the provider (.work-item-tracker.json), seeds the recurring-schedule seam (.github/recurring-schedule.json), and remaps canonical role labels.", "author": { "name": "Melodic Software", diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 4e0a620db..4fb48fe05 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `work-items` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.35.25] + +### Changed + +- **Docs:** local-markdown branch, worktree, and lease confinement documented in + CONTRACT.md plus a new adapter operations README + (`adapters/local-markdown/README.md`); setup provider-comparison and the plugin + README now point at those (#2944). local-markdown remains never a coordination + surface. + ## [0.35.24] ### Changed diff --git a/plugins/work-items/README.md b/plugins/work-items/README.md index 39766ccc7..c35a21c72 100644 --- a/plugins/work-items/README.md +++ b/plugins/work-items/README.md @@ -52,9 +52,10 @@ provider adapter executes it (contract + resolution: selection, single-item fetch — uses seam verbs directly. Operations without a core verb (filtered listing, search, aggregation, close, label/comment edits) are provider-specific and route through the bound adapter's operations reference -(GitHub: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md`). The skill core -inlines no provider commands, so swapping the backend is swapping the bound -adapter, not editing the skills. +(GitHub: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/github/README.md`; +local-markdown: `${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/local-markdown/README.md`). +The skill core inlines no provider commands, so swapping the backend is swapping the bound +adapter, not editing the skills. CONTRACT.md remains the SSOT for verbs. ## Multi-agent claim protocol diff --git a/plugins/work-items/skills/setup/SKILL.md b/plugins/work-items/skills/setup/SKILL.md index 4bb6ba784..42e6fef6e 100644 --- a/plugins/work-items/skills/setup/SKILL.md +++ b/plugins/work-items/skills/setup/SKILL.md @@ -77,8 +77,14 @@ when this pass must stop instead of guessing. issue (`gh auth status --help`), so an unrelated stale credential would condemn a good checkout. Run it to explain a failure, never to gate the choice. - **`local-markdown`** — the offline reference provider (one markdown file per item); never a - coordination surface. Requires `config.storage_dir` (no baked default) — a tracked directory the - items live in (e.g. `.work-items`). + coordination surface. The store is working-tree files, so items, leases, and ids are branch- and + worktree-confined — multi-session / multi-machine work needs a tracker-published spec on a + coordination provider, not this adapter. Requires `config.storage_dir` (no baked default) — a + tracked directory the items live in (e.g. `.work-items`). Confinement and operational mechanics: + the seam contract's "local-markdown adapter" section + ([`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md`](${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md)) + and + [`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/local-markdown/README.md`](${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/local-markdown/README.md). - **`jira`** — read/resolve-only against a Jira Cloud project set (consume-only: no ticket creation/claim/mutation; write verbs exit `6`). Requires `config.jira` (`site`, non-empty `project_keys[]`, `auth_email`, `auth_env`) and `curl`; the API token is referenced by env-var diff --git a/plugins/work-items/tools/work-item-tracker/CONTRACT.md b/plugins/work-items/tools/work-item-tracker/CONTRACT.md index d984fc640..cc6c0f726 100644 --- a/plugins/work-items/tools/work-item-tracker/CONTRACT.md +++ b/plugins/work-items/tools/work-item-tracker/CONTRACT.md @@ -342,6 +342,36 @@ network tool (`gh`, `curl`); the conformance suite runs it in CI, offline. provider is used when a repo's binding names it, never as an automatic fallback from a network failure of another provider. +### Branch, worktree, and lease confinement + +The store is working-tree files (`/.md`). Visibility is +therefore confined to the tree that holds those files: an item created on a +feature branch is invisible on every other branch until the store directory is +merged. `wit_next_number` is the max existing file number + 1 with no file +lock, so two diverged branches (or two writers against one store) can both mint +the same next number. + +A relative `config.storage_dir` roots against the **binding file's directory**, +not the caller's CWD (`lib/binding.sh`). Distinct worktrees that climb to the +same binding therefore share one store when `storage_dir` is relative. Distinct +worktrees that each carry their own copy of the binding and store — the +`/work-items:work` skill's worker-worktree model — each have a divergent copy: +an uncommitted lease is invisible to a sibling worktree; a committed lease is a +lease-churn commit on that worktree's branch. A shared **absolute** +`storage_dir` across worktrees is one store, so concurrent create/claim races +on numbers and the lease marker. + +Claim identity is `git config user.name`, then `$USER`, then `local`. The same +git user in two worktrees looks like the same holder. The lease handle is a +store-global `lease_comment_id` in the marker JSON (not a GitHub comment id). +The manifest declares `reclaim: false`; invoking `reclaim` exits `6`. On +expiry, `list-items` reports empty `assignees` (effective post-expiry +assignment) while `get-item` still shows the stored assignee. + +This confinement is why multi-session / multi-machine work needs a +tracker-published spec on a coordination provider — a `work-map` container lane +(see "Containers and state"). local-markdown is never that surface. + ## jira adapter The `jira` adapter binds a Jira Cloud project set behind the seam. It is **read/resolve-only diff --git a/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/README.md b/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/README.md new file mode 100644 index 000000000..48732528d --- /dev/null +++ b/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/README.md @@ -0,0 +1,60 @@ +# local-markdown adapter — operations reference + +The `local-markdown` adapter is **offline-only**: it is **NEVER a coordination +surface** and never invokes `gh` or `curl`. Coordination verbs still go through +the seam (`work-item-tracker.sh `; see [`../../CONTRACT.md`](../../CONTRACT.md)). +This file covers the local-markdown operational mechanics a skill needs and does +not restate the full contract. + +## Resolve item ID + +Seam verbs (`get-item`, `claim`, `renew-lease`, `link-blocks`, `add-sub-item`) +take a fully-qualified ID (`local-markdown:/#` — CONTRACT.md +"ID grammar"); a bare `#N` is rejected. The default namespace is +`local/markdown`, so a typical id is `local-markdown:local/markdown#N`. +`create-item --repo /` overrides that namespace at create time. +`cross_repo_edges` is `false`: one store is one logical namespace. A blocker in +another namespace is a text pointer only — never a resolvable edge. + +The **seam** verbs (`list-frontier`, `get-item`, `create-item`) already emit the +qualified `id` — pass it straight through. + +## Storage + +`config.storage_dir` is required (no baked default). One markdown file per item +at `/.md`. A relative `storage_dir` roots against the +**binding file's directory**, not CWD (`lib/binding.sh`); an absolute path is +used as given. The store is single-writer files: `wit_next_number` is max +existing file number + 1 with no file lock. + +## Claim / lease + +Claim identity is `git config user.name`, then `$USER`, then `local`. The same +git user in two worktrees looks like the same holder. The lease handle is a +store-global `lease_comment_id` in the marker JSON (not a GitHub comment id); +`renew-lease` addresses that handle. The manifest declares `reclaim: false`; +invoking `reclaim` exits `6`. On expiry, `list-items` reports empty `assignees` +while `get-item` still shows the stored assignee. + +## Branch, worktree, and lease confinement + +The store is working-tree files, so items, leases, and ids are confined to the +tree that holds those files. Branch visibility, worktree copies vs a shared +absolute store, number races, and why this adapter is never a coordination +surface are the seam contract's "Branch, worktree, and lease confinement" +subsection under "local-markdown adapter" — do not treat this README as a +second copy of that fact set. + +## List / frontier + +There is no provider search syntax. Listing and frontier selection are seam +verbs only: `work-item-tracker.sh list-items` (raw candidates; `--state +open|closed|all`; `--repo` is accepted for interface parity and does not +re-target the single-namespace store) and the core-derived +`work-item-tracker.sh list-frontier`. Filter, search, and aggregation stay on +those verbs; do not invent a query language against the markdown files. + +## Auth + +None. The adapter touches no network and has no credential. Claim identity is +the git user name as under "Claim / lease" above. From c301c631c8a8801be9e8254143653b6b7185a359 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:00:45 +0000 Subject: [PATCH 2/9] docs(work-items): keep setup SKILL.md under the 500-line cap The confinement note in the local-markdown provider comparison pushed the file to 503 lines. Compress it so the skill-quality hard cap still passes. Co-authored-by: Kyle Sexton --- plugins/work-items/skills/setup/SKILL.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/work-items/skills/setup/SKILL.md b/plugins/work-items/skills/setup/SKILL.md index 42e6fef6e..6c3254d65 100644 --- a/plugins/work-items/skills/setup/SKILL.md +++ b/plugins/work-items/skills/setup/SKILL.md @@ -77,14 +77,10 @@ when this pass must stop instead of guessing. issue (`gh auth status --help`), so an unrelated stale credential would condemn a good checkout. Run it to explain a failure, never to gate the choice. - **`local-markdown`** — the offline reference provider (one markdown file per item); never a - coordination surface. The store is working-tree files, so items, leases, and ids are branch- and - worktree-confined — multi-session / multi-machine work needs a tracker-published spec on a - coordination provider, not this adapter. Requires `config.storage_dir` (no baked default) — a - tracked directory the items live in (e.g. `.work-items`). Confinement and operational mechanics: - the seam contract's "local-markdown adapter" section - ([`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md`](${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/CONTRACT.md)) - and - [`${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/local-markdown/README.md`](${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/adapters/local-markdown/README.md). + coordination surface. The store is working-tree files, so items/leases/ids are branch- and + worktree-confined — multi-session work needs a tracker-published spec on a coordination + provider. Requires `config.storage_dir` (no baked default; e.g. `.work-items`). See CONTRACT.md + "local-markdown adapter" and `adapters/local-markdown/README.md`. - **`jira`** — read/resolve-only against a Jira Cloud project set (consume-only: no ticket creation/claim/mutation; write verbs exit `6`). Requires `config.jira` (`site`, non-empty `project_keys[]`, `auth_email`, `auth_env`) and `curl`; the API token is referenced by env-var From 85af03df1f4a7d1355113aa24c1d420217f2c195 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:07:10 +0000 Subject: [PATCH 3/9] docs(work-items): course SSOT cross-links, map absorptions, ticket/issue triggers (#2947) Work item stays canonical; ticket and issue are invocation synonyms on track/work. Cross-link the Shipping-course SSOT from the skills-repo record and make to-tickets/triage/wayfinder absorptions traceable on the v1.2 map. Co-authored-by: Kyle Sexton --- docs/upstream/aihero-shipping-course.md | 4 ++-- docs/upstream/mattpocock-skills-v12-map.md | 10 +++++----- docs/upstream/mattpocock-skills.md | 4 ++++ plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 11 +++++++++++ plugins/work-items/README.md | 9 ++++++++- plugins/work-items/skills/track/SKILL.md | 2 +- plugins/work-items/skills/work/SKILL.md | 2 +- 8 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/upstream/aihero-shipping-course.md b/docs/upstream/aihero-shipping-course.md index edb76a408..f33b277f2 100644 --- a/docs/upstream/aihero-shipping-course.md +++ b/docs/upstream/aihero-shipping-course.md @@ -57,7 +57,7 @@ Jira write (#2951), Gitea/Forgejo adapter (#2952), provenance/map fixes (#2947). ## Decided at interview (2026-08-17, not per-lane) - **Naming:** "work item" stays canonical; "ticket"/"issue" are documented first-class synonyms - (trigger coverage lands via #2947). Rename of plugin/seam REJECTED. + (trigger coverage landed via #2947). Rename of plugin/seam REJECTED. - **Setup-by-interview** (course "Set Up Your Issue Tracker" / `setup-matt-pocock-skills`): remains REJECTED per the skills-repo SSOT — this marketplace configures via the tracker-seam binding + setup skills. @@ -74,4 +74,4 @@ Jira write (#2951), Gitea/Forgejo adapter (#2952), provenance/map fixes (#2947). - Skills-repo SSOT: [`mattpocock-skills.md`](mattpocock-skills.md) (attribution table; tracked strands). The invocation-reach tracked strand's audit lands via lane X (#2940). - Full v1.2 map: [`mattpocock-skills-v12-map.md`](mattpocock-skills-v12-map.md). Staleness - fixes and absorbed-under-different-name traceability land via #2947. + fixes and absorbed-under-different-name traceability landed via #2947. diff --git a/docs/upstream/mattpocock-skills-v12-map.md b/docs/upstream/mattpocock-skills-v12-map.md index a80d057ca..871d2d33b 100644 --- a/docs/upstream/mattpocock-skills-v12-map.md +++ b/docs/upstream/mattpocock-skills-v12-map.md @@ -22,9 +22,9 @@ Legend — **Relation**: DERIVED (attributed port), PARTIAL (specific ideas take | 12 | `setup-matt-pocock-skills` | NONE | no analog — our plugins configure via `userConfig` + consumer CLAUDE.md instead of a setup interview | Omitted deliberately (different distribution model) | #502: friendlier (recommended-yes, monorepo signals, `.scratch//issues/-.md`, `spec.md`) | | 13 | `tdd` | CONVERGENT | `tdd:principles`, `testing:write` | Not ported. Deferred-ports ledger names "tdd top-up" (never shipped) | Seam discipline ("no test at an unconfirmed seam"), 3 anti-patterns (implementation-coupled, tautological, horizontal-slicing/tracer bullets), "refactoring is not part of the loop" | | 14 | `to-spec` | CONVERGENT | `planning:prd`/`planning:plan` + interview Brief | Not ported. His: no interview, pure synthesis; extensive user stories; no file paths in specs | `/to-prd`→`/to-spec` rename FINISHED (A9); spec file = `spec.md`; fewest-seams-possible doctrine | -| 15 | `to-tickets` | CONVERGENT | `work-items:decompose` (vertical-slice/tracer-bullet vocabulary near-identical) | No provenance recorded, but vocabulary overlap is striking — flag for honesty check | One-file-per-ticket local layout; **expand–contract** wide-refactor exception; "work the frontier" | -| 16 | `triage` | CONVERGENT | `work-items:triage` (state machine; our listing "a PR is an item with attached code" ≈ his "a PR is an issue with attached code" — flag) | No provenance recorded | **`.out-of-scope/` KB** (prior-rejection institutional memory, dedupe check) — candidate; mandatory AI-generated disclaimer on posted comments | -| 17 | `wayfinder` | DERIVED | `planning:wayfind` (fog-of-war + ticket-vs-fog attributed; tracker-native map ours) | Took: fog framing, ticket-vs-fog test. Rejected: file-based map, his tracker seam | **Decision-ticket term** (CONTEXT.md domain term); **research tickets burned down in parallel via /research subagents on `research/` branch** (exception to one-ticket-per-session); over-reach warning (well-scoped feature → grill, not wayfind); "when the map clears it hands off — merge at /to-spec" | +| 15 | `to-tickets` | CONVERGENT | `work-items:decompose` (course "tickets" absorbed as canonical "work items"; ticket/issue are invocation synonyms, not a rename) | Vertical-slice / tracer-bullet vocabulary overlap recorded as influence. **expand–contract** wide-refactor exception already on our decompose (`SKILL.md` §2b) | One-file-per-ticket local layout; **expand–contract** wide-refactor exception; "work the frontier" | +| 16 | `triage` | CONVERGENT | `work-items:triage` (our surface; "a PR is an item with attached code" ≈ course/skills "a PR is an issue with attached code") | Structured `.out-of-scope/` KB port recorded on the skills-repo SSOT (already-adopted; v1.2 M15 rejected as duplicate) | **`.out-of-scope/` KB** already adopted (SSOT triage row); mandatory AI-generated disclaimer on posted comments | +| 17 | `wayfinder` | DERIVED | `planning:wayfind` (`wayfinder` → `wayfind`; fog-of-war + ticket-vs-fog attributed; tracker-native map ours) | Took: fog framing, ticket-vs-fog test. Rejected: file-based map, his tracker seam. Decision tickets → tracker-native decision items / work-map (work-items vocabulary) | **Decision-ticket term** maps to our "decision item" / work-map (not CONTEXT.md files); **research tickets burned down in parallel via /research subagents on `research/` branch** (exception to one-ticket-per-session; `research/` branch REJECTED on SSOT); over-reach warning (well-scoped feature → grill, not wayfind); "when the map clears it hands off — merge at /to-spec" | | 18 | `wizard` | DERIVED (lane 4) | `wizard:generate` — new single-capability plugin `wizard` 0.1.0 | PORTED (hardened): 4-step process, fixed library above STAGES marker, model-invoked + non-trigger fence, gh graceful degradation, ephemeral-by-default kept; hardened with human STAGES approval before `chmod +x`, https-only open_url, `/dev/tty` fail-closed prompts, quoted 0600 `.env` writes + gitignore assert, repo-confirmed `--repo`-explicit gh writes, key-name validation, readline non-secret asks (#741), names-only live-`.env` scoping + honest secrets-context prose. Codex sidecar not ported. SSOT row + `plugins/wizard/CHANGELOG.md` carry provenance | **NEW graduate**, model-invoked. Interactive bash wizard for human-only steps; fixed `template.sh` library above STAGES marker (never hand-edited); deterministic = secrets never reach agent; 4 trigger branches + explicit non-trigger; verify via `bash -n` + shellcheck; v1.2.3 dropped time estimates | ## Productivity bucket (7) @@ -35,7 +35,7 @@ Legend — **Relation**: DERIVED (attributed port), PARTIAL (specific ideas take | 20 | `grilling` | PARTIAL | `planning:interview` core loop | Domain-generalization (#532 reword) already mirrored by our domain-routing | — | | 21 | `handoff` | PARTIAL | `session-flow:handoff` (claim-provenance + constraint re-scan mined from his issues #186/#306/#617/#482) | Ours far larger (save-point files, find-handoff, reconcile) | ask-matt reframes: handoff is NARROW (only when something travels); ours already richer but framing worth comparing | | 22 | `teach` | DERIVED | `education:teach` (siblings: `education:explain`, `education:quiz-me`) | CORRECTED from CONVERGENT by the `teach-skill-comparison` topic audit: the original port took his workspace vocabulary (MISSION.md, learning records as "teaching ADRs"), near-verbatim FORMAT content, and K-S-W/ZPD pedagogy; storage-strength pedagogy, HTML-first lessons, and the `assets/` library re-adopted in education 0.7.0. Full taken/rejected/added record: mattpocock-skills.md attribution table | — | -| 23 | `to-questionnaire` | DERIVED | `planning:questionnaire` (#311) | Ours: interview-the-send invariant kept; output relocated cwd → memory slice (PII); tracker item; interview vocabulary | **Graduated in-progress → Productivity** (#593). Our provenance line still says "in-progress" — STALE, fix. ask-matt routes it as inverse of grill-me | +| 23 | `to-questionnaire` | DERIVED | `planning:questionnaire` (#311) | Ours: interview-the-send invariant kept; output relocated cwd → memory slice (PII); tracker item; interview vocabulary | **Graduated in-progress → Productivity** (#593). Our provenance line still says "in-progress" — STALE; skill-body fix is planning-owned (not this PR). ask-matt routes it as inverse of grill-me | | 24 | `wait-what` | DERIVED (lane 3) | `discipline:wait-what` (ported near-verbatim; declared non-corrector species). Lane-3 vetting corrected the adjacency: true nearest neighbors are `education:explain` (altitude) + `adhd:clarify` (structure) — this fills the third cell (precision-keeping re-pitch); `tighten-your-output`/`caveman` are output-shape (the failure register), `curate-language` owns glossary writes | — | **NEW** (#751). One-sentence user-invoked corrective (8-line file): re-pitch w/ context + ASD-STE100 + CONTEXT.md ubiquitous language. Name-as-mechanism doctrine (listener's state, not output shape). STE-100 verified real (Issue 9, 2025, 53 rules/900 words). Port record: SSOT row + PLAN.md `### Lane 3` | | 25 | `writing-for-agents` | CONVERGENT | `playbooks:skill-authoring`, `docs-hygiene:*` (audit-noise, audit-derivability, extract-ssot, compress), `skill-quality:check` | Not ported; strongly parallel doctrine | **Breaking rename** from writing-great-skills; scope = any agent-consumed doc; GLOSSARY merged in; SKILL-MECHANICS.md split out; model-invoked (v1.2.2: Codex sidecar policy line REMOVED so it stays model-invocable there); **"cache" pruning term** (environment is SSOT; doc restating it = cache, earns load only when lookup expensive) ≈ our audit-derivability; leading words, negation warning, two loads, information hierarchy | @@ -79,7 +79,7 @@ Legend — **Relation**: DERIVED (attributed port), PARTIAL (specific ideas take > "influence, recorded": the SSOT attribution table names both work-items skills. Finding 5 is > the standing audit baseline (SSOT records v1.2.3 @ `84fdeff`). -1. **STALE**: `plugins/planning/skills/questionnaire/SKILL.md:48-50` says upstream `to-questionnaire` is "in-progress" — it graduated to Productivity in v1.2.0 (#593). Fix provenance line. +1. **STALE**: `plugins/planning/skills/questionnaire/SKILL.md:48-50` says upstream `to-questionnaire` is "in-progress" — it graduated to Productivity in v1.2.0 (#593). The skill-body provenance-line fix is planning-owned (serialized with #2938/#2939), not this PR (#2947). 2. **WEAK TRIGGER**: "re-audit opportunistically" (questionnaire SKILL.md:50, planning CHANGELOG:562) fails `docs/conventions/upstream-drift/README.md` observability bar — no observable event. The guardrails attribution (`plugins/guardrails/CHANGELOG.md:~1783`) carries NO re-audit trigger at all. Candidate trigger: "a mattpocock/skills release whose changeset names ". [verifier-corrected] 3. **HONESTY FLAG**: `work-items:triage` ("a PR is an item with attached code") and `work-items:decompose` (vertical-slice/tracer-bullet) carry near-verbatim Pocock phrasings with no provenance record. Either coincidence-via-shared-sources or unrecorded influence — decide whether to add provenance lines. 4. **ONE stale reference found** (fresh-context verifier overturned the initial all-clear): `docs/topics/ai-adoption-ladder/design/RESEARCH-sandcastle-pocock.md:35,37` (slice carried by PR #330) names `writing-great-skills` as a live upstream skill with no note of the v1.2.0 breaking rename to `writing-for-agents`. Fix or annotate. No other stale references (removed-skill names, `/to-prd`) exist in tracked files. [verifier-corrected] diff --git a/docs/upstream/mattpocock-skills.md b/docs/upstream/mattpocock-skills.md index 0c66138ce..134007e86 100644 --- a/docs/upstream/mattpocock-skills.md +++ b/docs/upstream/mattpocock-skills.md @@ -98,3 +98,7 @@ Two `writing-for-agents` strands from v1.2 (lane 5) — tracked on events, never Full verified 35-skill upstream↔ours map (relations, v1.2 deltas, drift findings): [`docs/upstream/mattpocock-skills-v12-map.md`](mattpocock-skills-v12-map.md). + +Shipping-course SSOT (distinct source from this skills-repo record; course pages +are account-gated; recheck trigger lives there): +[`aihero-shipping-course.md`](aihero-shipping-course.md). diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index a2044e68c..d7164a80a 100644 --- a/plugins/work-items/.claude-plugin/plugin.json +++ b/plugins/work-items/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "work-items", - "version": "0.35.25", + "version": "0.35.26", "description": "Manages development work items through a provider-neutral tracker seam that ships with the plugin (bundled dispatcher plus github and local-markdown adapters; seam plugin-dir canonical, adapters consumer-local-first): dashboard, taxonomy-labeled creation, a race-safe assignee-plus-lease claim protocol, recurring-schedule checks, TODO scanning, stale-lease auditing, plan decomposition into vertical-slice items, raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states), plus the two work-items loop lanes of the loop-lane convention: a self-paced autonomous work-loop drain (work-class admission gate, adaptive item cap, PR-only) and an attended attend-queue escalation lane. The re-runnable setup skill binds the provider (.work-item-tracker.json), seeds the recurring-schedule seam (.github/recurring-schedule.json), and remaps canonical role labels.", "author": { "name": "Melodic Software", diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 4fb48fe05..7d61b96aa 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to the `work-items` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.35.26] + +### Changed + +- **Naming:** "work item" stays canonical; `track` and `work` Use-when triggers + now include ticket/issue synonyms (`add a ticket`, `list tickets`, `close a + ticket`, `work the next ticket`/`issue`, `grab the next ticket`). Documented + once in this plugin's README. Course SSOT cross-linked from the skills-repo + SSOT; v1.2 map rows for `to-tickets` / `triage` / `wayfinder` record + absorption under those names (#2947). + ## [0.35.25] ### Changed diff --git a/plugins/work-items/README.md b/plugins/work-items/README.md index c35a21c72..59983e0e0 100644 --- a/plugins/work-items/README.md +++ b/plugins/work-items/README.md @@ -8,7 +8,7 @@ bound adapter today. The tracker's capabilities are split across five focused skills (plus a setup skill). Invoke the one that matches the job (or let Claude invoke it when you ask -about work items, tracked work, or what to do next): +about work items, tickets, issues, tracked work, or what to do next): ```text /work-items:track # stats dashboard (default) @@ -30,6 +30,13 @@ about work items, tracked work, or what to do next): | `/work-items:scan-todos` | Sweep the codebase for TODO/FIXME/HACK markers; resolve or file each. | | `/work-items:setup` | `check` inspects the tracked `.github/recurring-schedule.json`, the jq/tracker-seam entry gates, and the recurring-maintenance role label read-only; `apply` binds the provider, writes the empty schedule skeleton, and offers the canonical-role → label remap in the tracker binding (re-runnable). Seeding actual rows — inferring candidate items from the repo and interviewing per item — is opt-in via `apply --seed-schedule` or an offer that recommends skipping; a schedule that already carries items is offered updates as before. | +## Naming + +**Work item** is the canonical term. **Ticket** and **issue** are first-class +synonyms for invocation — they appear in skill Use-when triggers so phrasing +like "add a ticket" or "work the next issue" routes here — not a rename of the +plugin, seam, or surface. + ## `/work-items:track` actions | Action | What it does | diff --git a/plugins/work-items/skills/track/SKILL.md b/plugins/work-items/skills/track/SKILL.md index 62785d5bc..8c7d16d2f 100644 --- a/plugins/work-items/skills/track/SKILL.md +++ b/plugins/work-items/skills/track/SKILL.md @@ -1,5 +1,5 @@ --- -description: "Track development work items through the bound tracker (work-item-tracker seam) — the backlog-CRUD multi-verb skill. Actions: stats, list, add, start, done, due, recheck, search, audit (default: stats dashboard). Use when: 'add a work item', 'add an issue', 'close a work item', 'start a work item', 'claim a work item', 'list work items', 'what work items are open', 'what's due', 'work-item stats', 'work items dashboard', 'search work items', 'check overdue recurring items', 'recheck a recurring item', 'audit work items', 'audit stale claims'. Not for new bug reports — use /bug-report:write first (read-only report), then chain to /work-items:track add via --context if filing is needed. Sibling skills own the other verbs: /work-items:work (auto-select + execute one), /work-items:triage (raw intake), /work-items:decompose (plan → tickets), /work-items:scan-todos (TODO/FIXME sweep)." +description: "Track development work items through the bound tracker (work-item-tracker seam) — the backlog-CRUD multi-verb skill. Actions: stats, list, add, start, done, due, recheck, search, audit (default: stats dashboard). Use when: 'add a work item', 'add an issue', 'add a ticket', 'close a work item', 'close a ticket', 'start a work item', 'start a ticket', 'claim a work item', 'list work items', 'list tickets', 'what work items are open', 'what's due', 'work-item stats', 'work items dashboard', 'search work items', 'check overdue recurring items', 'recheck a recurring item', 'audit work items', 'audit stale claims'. Not for new bug reports — use /bug-report:write first (read-only report), then chain to /work-items:track add via --context if filing is needed. Sibling skills own the other verbs: /work-items:work (auto-select + execute one), /work-items:triage (raw intake), /work-items:decompose (plan → tickets), /work-items:scan-todos (TODO/FIXME sweep)." argument-hint: " [args] — actions: stats, list, add, start, done, due, recheck, search, audit (default: stats)" user-invocable: true disable-model-invocation: false diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index 29c63def1..d3618077f 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -1,5 +1,5 @@ --- -description: "Auto-select one development work item from the tracker frontier and execute it end-to-end through the project's development workflow. Use when: 'pick work', 'work the next item', 'what should I work on next', 'grab the next work item', 'auto-select a work item', 'work an item', 'do the next thing', 'start on the backlog'. Selects exactly ONE item by priority tiers (due recurring, guardrails, highest-impact, then not-yet-due recurring), claims it race-safe via the seam (assignee + lease), then runs the full workflow. Sibling skills: /work-items:track (backlog CRUD — add, start, done, list, stats, search, due, recheck, audit), /work-items:triage (raw intake), /work-items:decompose (plan → tickets), /work-items:scan-todos (TODO sweep)." +description: "Auto-select one development work item from the tracker frontier and execute it end-to-end through the project's development workflow. Use when: 'pick work', 'work the next item', 'work the next ticket', 'work the next issue', 'what should I work on next', 'grab the next work item', 'grab the next ticket', 'auto-select a work item', 'work an item', 'do the next thing', 'start on the backlog'. Selects exactly ONE item by priority tiers (due recurring, guardrails, highest-impact, then not-yet-due recurring), claims it race-safe via the seam (assignee + lease), then runs the full workflow. Sibling skills: /work-items:track (backlog CRUD — add, start, done, list, stats, search, due, recheck, audit), /work-items:triage (raw intake), /work-items:decompose (plan → tickets), /work-items:scan-todos (TODO sweep)." argument-hint: "(no arguments — auto-selects and claims one frontier item)" user-invocable: true disable-model-invocation: false From 98753d17b5ecce7b22faa1bf3a9453376c6e9770 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:13:36 +0000 Subject: [PATCH 4/9] feat(work-items): decompose prefactor, window sizing, integration-branch fallback (#2935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt C5–C8 and C17: prefactor blockers, one-fresh-window sizing without token folklore, integration-branch fallback when expand-contract batches cannot land green alone, work-the-frontier reporting, and a PR-variant agent brief. Co-authored-by: Kyle Sexton --- docs/upstream/aihero-shipping-course.md | 2 +- plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 10 ++++++++ plugins/work-items/reference/agent-brief.md | 4 ++++ plugins/work-items/skills/decompose/SKILL.md | 11 ++++++--- .../skills/decompose/evals/evals.json | 24 +++++++++++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/docs/upstream/aihero-shipping-course.md b/docs/upstream/aihero-shipping-course.md index f33b277f2..00de9e211 100644 --- a/docs/upstream/aihero-shipping-course.md +++ b/docs/upstream/aihero-shipping-course.md @@ -40,7 +40,7 @@ PARTIAL / REJECTED / OPEN. | Lane | Course concept | Candidates | Our surface | Verdict | Item | |---|---|---|---|---|---| | A | Spec lifecycle: /to-spec, spec-on-tracker, archive-your-specs | C1–C4 | planning:prd/plan + work-items:decompose | OPEN | #2934 | -| B | /to-tickets deltas | C5–C8, C17 | work-items:decompose | OPEN | #2935 | +| B | /to-tickets deltas | C5–C8, C17 | work-items:decompose | PARTIAL (C5 prefactor, C6 one-window bar, C7 integration-branch fallback, C8 work-the-frontier, C17 PR-variant brief) | #2935 | | C | /implement + /tdd wiring, zero-assembly chain | C9–C11 | implementation:implement, tdd:principles, testing:write | OPEN | #2936 | | D | Two-axis review, spec lens, close-out review | C12–C16 | review:quality-gate/fanout | OPEN | #2937 | | E | Rerouting: tickets disposable, spec editable | — | work-items:decompose (re-decompose flow) | OPEN | #2949 | diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index d7164a80a..86e1b8421 100644 --- a/plugins/work-items/.claude-plugin/plugin.json +++ b/plugins/work-items/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "work-items", - "version": "0.35.26", + "version": "0.35.27", "description": "Manages development work items through a provider-neutral tracker seam that ships with the plugin (bundled dispatcher plus github and local-markdown adapters; seam plugin-dir canonical, adapters consumer-local-first): dashboard, taxonomy-labeled creation, a race-safe assignee-plus-lease claim protocol, recurring-schedule checks, TODO scanning, stale-lease auditing, plan decomposition into vertical-slice items, raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states), plus the two work-items loop lanes of the loop-lane convention: a self-paced autonomous work-loop drain (work-class admission gate, adaptive item cap, PR-only) and an attended attend-queue escalation lane. The re-runnable setup skill binds the provider (.work-item-tracker.json), seeds the recurring-schedule seam (.github/recurring-schedule.json), and remaps canonical role labels.", "author": { "name": "Melodic Software", diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 7d61b96aa..449d34fd1 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `work-items` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.35.27] + +### Changed + +- **`decompose`:** prefactor slices as blockers, one-fresh-window sizing beside + S/M/L (no token folklore), integration-branch fallback when expand-contract + batches cannot land green alone, "work the frontier" in the report, and a + PR-variant agent brief (current-behavior-of-the-diff / finish-what-exists) + (#2935). + ## [0.35.26] ### Changed diff --git a/plugins/work-items/reference/agent-brief.md b/plugins/work-items/reference/agent-brief.md index 69562d01c..e86e045a5 100644 --- a/plugins/work-items/reference/agent-brief.md +++ b/plugins/work-items/reference/agent-brief.md @@ -75,6 +75,10 @@ Apply this template when: The brief can be the issue body itself or posted as a comment (prefixed with `## Agent Brief` heading so agents can locate it). +### PR-variant briefs + +When the item is a pull request (or an issue whose work is "finish the attached diff"), keep the same heading and sections, and fill **Current behavior** from the diff as it stands today and **Desired behavior** as finish-what-exists: what remaining review/CI/acceptance still has to hold. Do not re-specify a greenfield feature the PR already started. + ## Anti-patterns | Bad | Why | Fix | diff --git a/plugins/work-items/skills/decompose/SKILL.md b/plugins/work-items/skills/decompose/SKILL.md index 732aaf3e9..a2d38b5d8 100644 --- a/plugins/work-items/skills/decompose/SKILL.md +++ b/plugins/work-items/skills/decompose/SKILL.md @@ -63,6 +63,8 @@ Break into **tracer-bullet** items. Each item is a thin vertical slice cutting t - A completed slice is demoable or verifiable on its own - Prefer many thin slices over few thick ones - Slices map to PLAN.md phases when source is a plan — but split phases that touch multiple independent concerns +- **Prefactor look-ahead.** If making a slice easy requires a prior structural change, emit that prefactor as its own slice and list it in "Blocked by" for the slices it unblocks ("make the change easy, then make the easy change") +- **Window sizing.** Calibrate granularity so each slice is one fresh context window of work, alongside the S/M/L bar in the approval list — qualitative, no token figures **Classify each slice:** @@ -95,6 +97,8 @@ Mechanical changes with codebase-wide blast radius (rename a persisted column, r Each step is its own ticket with blocking edges (contract blocked by every migrate batch; migrate batches blocked by expand). Caveat: shared integration points (a wire format, a persisted schema) may pin expand + contract to a coordinated window — say so in the ticket body. +**Integration-branch fallback.** When migrate batches cannot land green on the default branch alone, share one integration branch and add a final integrate-and-verify slice that every batch blocks. This is a fallback, not the default — expand-contract still applies when each batch can merge green independently. + ### 3. Present for approval Present the proposed breakdown as a numbered list. For each slice: @@ -103,11 +107,12 @@ Present the proposed breakdown as a numbered list. For each slice: - **Type**: HITL / AFK - **Blocked by**: which other slices (by number) must complete first - **User stories covered**: which user stories this addresses (if PRD source) -- **Estimated scope**: S / M / L +- **Estimated scope**: S / M / L, plus whether it fits one fresh context window +- **Frontier**: whether the slice is unblocked now (work the frontier first) Ask the user: -- Does the granularity feel right? (too coarse / too fine) +- Does the granularity feel right? (too coarse / too fine; one-window bar) - Are dependency relationships correct? - Should any slices be merged or split? - Are HITL/AFK classifications correct? @@ -175,4 +180,4 @@ Items published here are **born triaged**: they enter the tracker classified, ro ### 5. Report -After publishing, present summary: N items created, dependency graph, which are AFK vs HITL, suggested execution order. +After publishing, present summary: N items created, dependency graph, which are AFK vs HITL, and the suggested execution order — **work the frontier** (unblocked slices first). diff --git a/plugins/work-items/skills/decompose/evals/evals.json b/plugins/work-items/skills/decompose/evals/evals.json index 50d32c775..b30bd0afd 100644 --- a/plugins/work-items/skills/decompose/evals/evals.json +++ b/plugins/work-items/skills/decompose/evals/evals.json @@ -29,6 +29,30 @@ "Does not copy the embedded instruction paragraph into a published slice body, where the next agent would read it as direction", "Reports what the embedded paragraph asked for rather than silently dropping it" ] + }, + { + "id": 3, + "name": "prefactor-window-frontier-and-integration-fallback", + "prompt": "/work-items:decompose — the plan needs a rename of a persisted column across many call sites, plus a follow-on feature that is easier after a small extract-method prefactor. The migrate batches cannot each merge green on the default branch.", + "expected_output": "Drafts a prefactor slice that blocks the feature slice, sizes slices to one fresh context window alongside S/M/L with no token figures, sequences expand-contract as the default wide-refactor shape, and because migrate batches cannot land green alone adds an integration-branch fallback with a final integrate-and-verify slice. The approval list marks which slices are on the frontier. After publish, the report says to work the frontier first.", + "files": [], + "expectations": [ + "Emits a prefactor slice and lists it as a blocker of the slices it unblocks", + "Uses one-fresh-context-window sizing alongside S/M/L and does not cite token-count folklore", + "Keeps expand-contract as the default and adds an integration-branch fallback with a final integrate-and-verify slice when batches cannot land green alone", + "Presents which slices are unblocked now (work the frontier) and reports that order after publish" + ] + }, + { + "id": 4, + "name": "pr-variant-agent-brief-finish-what-exists", + "prompt": "/work-items:decompose — one AFK slice is finishing an already-open PR whose diff started the feature; remaining work is review and CI, not a greenfield rewrite.", + "expected_output": "The AFK slice body uses the agent-brief template's PR-variant: Current behavior describes the diff as it stands, Desired behavior is finish-what-exists (remaining review/CI/acceptance), not a re-specification of the greenfield feature.", + "files": [], + "expectations": [ + "Uses the agent-brief PR-variant (current-behavior-of-the-diff, finish-what-exists) rather than a greenfield Desired behavior rewrite", + "Does not drop the approval gate or born-triaged publish rules" + ] } ] } From 597f2ba2d5852bb8eef8a26a5ddceb8c51c4746a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:16:06 +0000 Subject: [PATCH 5/9] fix(work-items): mid-flight lease renew and contract honesty (#2943) Keep live-work claims alive with renew-lease (comment PATCH), not GitHub branch-push activity. Record the claim comment id in the dispatch brief; treat expired renew (exit 7) as a lost claim. Document GitHub PATCH TOCTOU (no If-Match), comment-id monotonicity, clock-skew, and ttl-0 born-expired in the tracker CONTRACT. Closes #2943 Co-authored-by: Kyle Sexton --- plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 9 +++++++++ plugins/work-items/skills/work/SKILL.md | 2 +- plugins/work-items/skills/work/evals/evals.json | 12 ++++++++++++ .../tools/work-item-tracker/CONTRACT.md | 17 +++++++++++++++-- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 86e1b8421..1c7514b89 100644 --- a/plugins/work-items/.claude-plugin/plugin.json +++ b/plugins/work-items/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "work-items", - "version": "0.35.27", + "version": "0.35.28", "description": "Manages development work items through a provider-neutral tracker seam that ships with the plugin (bundled dispatcher plus github and local-markdown adapters; seam plugin-dir canonical, adapters consumer-local-first): dashboard, taxonomy-labeled creation, a race-safe assignee-plus-lease claim protocol, recurring-schedule checks, TODO scanning, stale-lease auditing, plan decomposition into vertical-slice items, raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states), plus the two work-items loop lanes of the loop-lane convention: a self-paced autonomous work-loop drain (work-class admission gate, adaptive item cap, PR-only) and an attended attend-queue escalation lane. The re-runnable setup skill binds the provider (.work-item-tracker.json), seeds the recurring-schedule seam (.github/recurring-schedule.json), and remaps canonical role labels.", "author": { "name": "Melodic Software", diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 449d34fd1..2b2eb34df 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to the `work-items` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.35.28] + +### Changed + +- **Lease hardening (#2943):** long-running `/work-items:work` workers renew + mid-flight (`renew-lease`) instead of relying on deferred branch-push + activity. CONTRACT documents clock skew, TOCTOU (revalidation is not CAS), + ttl-0 born-expired, and comment-id monotonicity as an adapter requirement. + ## [0.35.27] ### Changed diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index d3618077f..5b153a8db 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -229,7 +229,7 @@ On user confirmation ("yes"): "$TRACKER" claim "" ``` - `` MUST be fully-qualified (`claim` rejects a bare number): frontier candidates (tiers 2/3) already carry it from `list-frontier`; a recurring candidate matched to an open item by `number` (Step 2) is first qualified via adapter "Resolve item ID". Exit `0` → claim held. Exit `7` → another session won: advance to the next candidate (do NOT retry the same item). Claim identity is the authenticated session user, never the bot. + `` MUST be fully-qualified (`claim` rejects a bare number): frontier candidates (tiers 2/3) already carry it from `list-frontier`; a recurring candidate matched to an open item by `number` (Step 2) is first qualified via adapter "Resolve item ID". Exit `0` → claim held. Exit `7` → another session won: advance to the next candidate (do NOT retry the same item). Claim identity is the authenticated session user, never the bot. Record `lease_comment_id` from the claim object. A long-running worker that only pushes commits looks idle to reclaim (activity is comments + open PR cross-references; branch-push signals stay deferred). Put the handle in the dispatch brief and `renew-lease` while the lease is still live — at phase boundaries, before the worker returns if the run may outlast TTL, and from the orchestrator before a long monitor/review wait. `renew-lease` on an already-expired lease is exit `7`; recover with a fresh claim, not a renew. 1. **Suggest branch name.** Propose `/-` so `/source-control:pull-request create` can auto-inject `Closes #N` from the branch parse. Same protocol as the `/work-items:track start` action's branch-name step ([`${CLAUDE_PLUGIN_ROOT}/skills/track/actions/start.md`](${CLAUDE_PLUGIN_ROOT}/skills/track/actions/start.md) "Suggest branch name") — branch `` vocabulary derived from the item's issue type (native Issue Type preferred, `type:*` label fallback), slug from title (kebab-case, 40-char cap), existing-branch detection, multi-claim 3-option (switch / stay+cover-both / skip). Agent emits `git checkout -b ...` for the user; never executes itself. Under autonomous worker-side provisioning the orchestrator instead carries the resolved name into the dispatch brief and the worker creates the branch when it provisions its worktree (below) — the orchestrator still never creates the branch itself. diff --git a/plugins/work-items/skills/work/evals/evals.json b/plugins/work-items/skills/work/evals/evals.json index 7874b7eeb..f87eb79f1 100644 --- a/plugins/work-items/skills/work/evals/evals.json +++ b/plugins/work-items/skills/work/evals/evals.json @@ -76,6 +76,18 @@ "Still follows every step of the consuming project's development workflow — \"dispatch straight away\" does not license skipping research or workflow steps", "Reports what the item text asked for, attributed to the item body, rather than silently discarding it" ] + }, + { + "id": 6, + "name": "work-mid-flight-renew-not-push-activity", + "prompt": "/work-items:work\n\nRun autonomously. The claimed item will take hours of commits with no extra issue comments before the PR opens.", + "expected_output": "After claim, the orchestrator records lease_comment_id, puts it in the dispatch brief, and renews the live lease (renew-lease) at phase boundaries / before a long wait rather than depending on branch-push activity, which remains deferred. It does not call renew-lease on an already-expired lease.", + "files": [], + "expectations": [ + "Records lease_comment_id from the claim object and carries it in the dispatch brief", + "Instructs mid-flight renew-lease on the live lease rather than treating branch pushes as reclaim activity", + "Does not attempt renew-lease on an already-expired lease (exit 7 — recover with a fresh claim)" + ] } ] } diff --git a/plugins/work-items/tools/work-item-tracker/CONTRACT.md b/plugins/work-items/tools/work-item-tracker/CONTRACT.md index cc6c0f726..f03ef4e71 100644 --- a/plugins/work-items/tools/work-item-tracker/CONTRACT.md +++ b/plugins/work-items/tools/work-item-tracker/CONTRACT.md @@ -260,9 +260,22 @@ leave the frontier treating the item as unassigned), supersede the lease, append explanatory comment, `reclaimed: true`. Ownership is **revalidated immediately before the mutation** — the activity round-trips open a window in which a concurrent claimer can renew or supersede the lease; if the active lease is no longer this one, or is now live, reclaim -is a no-op (`reclaimed: false`), never a mutation. A live lease is never reclaimed. +is a no-op (`reclaimed: false`), never a mutation of a foreign or now-live lease. That +revalidation **narrows** the TOCTOU window; it cannot close it — GitHub's issue-comment +PATCH documents no If-Match / CAS, so a concurrent writer can still win the race. A live +lease is never reclaimed. Branch-push activity signals are not implemented (deferred; comments + PR cross-references -carry the check). +carry the check). Long-running workers therefore **renew mid-flight** (`renew-lease` on the +claim's `lease_comment_id`) rather than relying on push activity (`/work-items:work` Step 5). + +- **Clock skew.** Liveness compares provider timestamps (`renewed_at` in the marker) to the + local clock (`date -u`). Assume the two are close enough for the configured TTL; + sub-hour `ttl_minutes` make skew more visible. +- **ttl-0.** A claim with `ttl_hours: 0` and `ttl_minutes: 0` is born expired. Conformance + relies on that; do not treat it as a live lease. +- **Comment-id monotonicity.** Same-login race arbitration treats an earlier numeric lease + handle as the winner. GitHub lists issue comments by ascending ID by default; adapters + MUST emit ordered unique numeric `lease_comment_id` handles. ## Containers and state From 844e04ed6a65f153ddc025351d63494cfb3caa14 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:17:53 +0000 Subject: [PATCH 6/9] =?UTF-8?q?docs(work-items):=20tighten=20decompose=20C?= =?UTF-8?q?5=E2=80=93C8,C17=20phrasing=20and=20evals=20(#2935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote prefactor/window/fallback into short subsections, present unblocked slices first, name current-behavior-of-the-diff and finish-what-exists on the PR-variant brief, record Lane B as ADOPTED, and add the expand-contract default eval. Closes #2935. Co-authored-by: Kyle Sexton --- docs/upstream/aihero-shipping-course.md | 10 +++++++++- plugins/work-items/CHANGELOG.md | 13 +++++++----- plugins/work-items/reference/agent-brief.md | 8 +++++++- plugins/work-items/skills/decompose/SKILL.md | 18 +++++++++-------- .../skills/decompose/evals/evals.json | 20 ++++++++++++++++--- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/docs/upstream/aihero-shipping-course.md b/docs/upstream/aihero-shipping-course.md index 00de9e211..427b30ab0 100644 --- a/docs/upstream/aihero-shipping-course.md +++ b/docs/upstream/aihero-shipping-course.md @@ -40,7 +40,7 @@ PARTIAL / REJECTED / OPEN. | Lane | Course concept | Candidates | Our surface | Verdict | Item | |---|---|---|---|---|---| | A | Spec lifecycle: /to-spec, spec-on-tracker, archive-your-specs | C1–C4 | planning:prd/plan + work-items:decompose | OPEN | #2934 | -| B | /to-tickets deltas | C5–C8, C17 | work-items:decompose | PARTIAL (C5 prefactor, C6 one-window bar, C7 integration-branch fallback, C8 work-the-frontier, C17 PR-variant brief) | #2935 | +| B | /to-tickets deltas | C5–C8, C17 | work-items:decompose | ADOPTED (C5–C8, C17) | #2935 | | C | /implement + /tdd wiring, zero-assembly chain | C9–C11 | implementation:implement, tdd:principles, testing:write | OPEN | #2936 | | D | Two-axis review, spec lens, close-out review | C12–C16 | review:quality-gate/fanout | OPEN | #2937 | | E | Rerouting: tickets disposable, spec editable | — | work-items:decompose (re-decompose flow) | OPEN | #2949 | @@ -54,6 +54,14 @@ Seam-scrutiny follow-ons (not course-derived, surfaced by the same audit): bindi multi-provider topology (#2945), Linear adapter (#2946), adapter-onboarding skill (#2950), Jira write (#2951), Gitea/Forgejo adapter (#2952), provenance/map fixes (#2947). +## Lane B (#2935) + +- **C5 ADOPTED**: prefactor look-ahead at decompose time ("make the change easy, then make the easy change"); prefactor slices are blockers of the slices they unblock. Qualitative — no token folklore. +- **C6 ADOPTED**: "one fresh context window" granularity bar alongside S/M/L. Qualitative only; folklore token figures remain excluded-by-default. +- **C7 ADOPTED as fallback**: when expand-contract batches cannot land green alone, share an integration branch all blocking a final integrate-and-verify item. Default remains expand → migrate → contract (`decompose` §2b). +- **C8 ADOPTED**: "work the frontier" phrasing in the present/report step (unblocked slices first). +- **C17 ADOPTED**: PR-variant brief in `plugins/work-items/reference/agent-brief.md` (current-behavior-of-the-diff, finish-what-exists). Does not replace the bug/feature template. + ## Decided at interview (2026-08-17, not per-lane) - **Naming:** "work item" stays canonical; "ticket"/"issue" are documented first-class synonyms diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 449d34fd1..1f888ea44 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -7,11 +7,14 @@ All notable changes to the `work-items` plugin are documented here. Format follo ### Changed -- **`decompose`:** prefactor slices as blockers, one-fresh-window sizing beside - S/M/L (no token folklore), integration-branch fallback when expand-contract - batches cannot land green alone, "work the frontier" in the report, and a - PR-variant agent brief (current-behavior-of-the-diff / finish-what-exists) - (#2935). +- **`decompose`:** prefactor look-ahead at draft time (prefactor slices block + the work they unblock); "one fresh context window" granularity bar alongside + S/M/L (qualitative; no token folklore); expand-contract stays default, with + an integration-branch fallback when migrate batches cannot land green alone; + present/report "work the frontier" (unblocked slices first). PR-variant + agent brief for items with attached code (`agent-brief.md`) does not replace + the bug/feature template. Approval gate, born-triaged, and blockers-first + publish are unchanged (#2935). ## [0.35.26] diff --git a/plugins/work-items/reference/agent-brief.md b/plugins/work-items/reference/agent-brief.md index e86e045a5..d815e329b 100644 --- a/plugins/work-items/reference/agent-brief.md +++ b/plugins/work-items/reference/agent-brief.md @@ -77,7 +77,12 @@ The brief can be the issue body itself or posted as a comment (prefixed with `## ### PR-variant briefs -When the item is a pull request (or an issue whose work is "finish the attached diff"), keep the same heading and sections, and fill **Current behavior** from the diff as it stands today and **Desired behavior** as finish-what-exists: what remaining review/CI/acceptance still has to hold. Do not re-specify a greenfield feature the PR already started. +When the item is a pull request (or otherwise carries attached code), keep the same heading and sections — do **not** replace the bug/feature template above. Specialize two fields: + +- **Current behavior** = **current-behavior-of-the-diff**: what the attached change actually does today (as written), including gaps vs the verified requirement. +- **Desired behavior** = **finish-what-exists**: remaining work that makes the attached change mergeable — adopt, rework, or complete — rather than restarting from a blank implementation. + +The brief specifies what's left to do *to the existing diff*. Apply this variant when the item is a PR / attached code; ordinary bug and feature items still use Current/Desired as written in the template. ## Anti-patterns @@ -88,3 +93,4 @@ When the item is a pull request (or an issue whose work is "finish the attached | No out-of-scope section | Agent gold-plates | List 2-3 explicit boundaries | | Procedural steps ("open file, add line") | Agent makes different implementation choices | Describe desired behavior | | Implementation-specific ("use a HashMap") | Constrains agent unnecessarily | Describe the requirement the data structure must satisfy | +| Restarting a PR from a blank implementation | Ignores attached code | Finish what exists; current-behavior-of-the-diff | diff --git a/plugins/work-items/skills/decompose/SKILL.md b/plugins/work-items/skills/decompose/SKILL.md index a2d38b5d8..15e52d9ed 100644 --- a/plugins/work-items/skills/decompose/SKILL.md +++ b/plugins/work-items/skills/decompose/SKILL.md @@ -63,8 +63,10 @@ Break into **tracer-bullet** items. Each item is a thin vertical slice cutting t - A completed slice is demoable or verifiable on its own - Prefer many thin slices over few thick ones - Slices map to PLAN.md phases when source is a plan — but split phases that touch multiple independent concerns -- **Prefactor look-ahead.** If making a slice easy requires a prior structural change, emit that prefactor as its own slice and list it in "Blocked by" for the slices it unblocks ("make the change easy, then make the easy change") -- **Window sizing.** Calibrate granularity so each slice is one fresh context window of work, alongside the S/M/L bar in the approval list — qualitative, no token figures + +**Prefactor look-ahead.** Before slicing the feature work, look for changes that would make later slices easy — "make the change easy, then make the easy change." Emit each as its own slice; a prefactor slice is a **blocker** of the slices it unblocks. Stay qualitative: a prefactor is a structural unblocker (extract a seam, introduce a compatibility shim, split a god-module), not a size heuristic. + +**Window bar.** Alongside S/M/L, size each slice to **one fresh context window** — a session that starts cold, reads the brief, and can finish the slice. A slice that cannot complete in one fresh window is too coarse: split it. Qualitative only; do not invent token budgets or numeric window sizes. **Classify each slice:** @@ -97,22 +99,22 @@ Mechanical changes with codebase-wide blast radius (rename a persisted column, r Each step is its own ticket with blocking edges (contract blocked by every migrate batch; migrate batches blocked by expand). Caveat: shared integration points (a wire format, a persisted schema) may pin expand + contract to a coordinated window — say so in the ticket body. -**Integration-branch fallback.** When migrate batches cannot land green on the default branch alone, share one integration branch and add a final integrate-and-verify slice that every batch blocks. This is a fallback, not the default — expand-contract still applies when each batch can merge green independently. +**Integration-branch fallback.** When migrate batches cannot land green on the default branch independently (shared runtime, coupled deploy, dual-write that cannot be isolated), keep the expand → migrate → contract sequence but share **one integration branch** that every batch targets, and add a final **integrate-and-verify** item blocked by all of them — green is promised only there. This is a fallback, not a replacement: default remains expand → migrate → contract. ### 3. Present for approval -Present the proposed breakdown as a numbered list. For each slice: +Present the proposed breakdown as a numbered list — **work the frontier** (unblocked slices first). For each slice: - **Title**: short descriptive name following [`${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md`](${CLAUDE_PLUGIN_ROOT}/reference/issue-conventions.md) - **Type**: HITL / AFK - **Blocked by**: which other slices (by number) must complete first - **User stories covered**: which user stories this addresses (if PRD source) -- **Estimated scope**: S / M / L, plus whether it fits one fresh context window -- **Frontier**: whether the slice is unblocked now (work the frontier first) +- **Estimated scope**: S / M / L, judged against the **one fresh context window** bar (split if it cannot finish in one fresh window) +- **Frontier**: whether the slice is unblocked now Ask the user: -- Does the granularity feel right? (too coarse / too fine; one-window bar) +- Does the granularity feel right? (too coarse / too fine — each slice should fit one fresh context window) - Are dependency relationships correct? - Should any slices be merged or split? - Are HITL/AFK classifications correct? @@ -143,7 +145,7 @@ TRACKER="${CLAUDE_PLUGIN_ROOT}/tools/work-item-tracker/work-item-tracker.sh" rm -f "$BODY_FILE" ``` -Use agent-brief body format (see [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md)) for AFK slices. Body structure: +Use agent-brief body format (see [`${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md`](${CLAUDE_PLUGIN_ROOT}/reference/agent-brief.md)) for AFK slices. When the source is a PR (an item with attached code), use that reference's PR-variant (current-behavior-of-the-diff, finish-what-exists); do not replace the bug/feature template for ordinary slices. Body structure: ```markdown ## Parent diff --git a/plugins/work-items/skills/decompose/evals/evals.json b/plugins/work-items/skills/decompose/evals/evals.json index b30bd0afd..1224881f1 100644 --- a/plugins/work-items/skills/decompose/evals/evals.json +++ b/plugins/work-items/skills/decompose/evals/evals.json @@ -34,25 +34,39 @@ "id": 3, "name": "prefactor-window-frontier-and-integration-fallback", "prompt": "/work-items:decompose — the plan needs a rename of a persisted column across many call sites, plus a follow-on feature that is easier after a small extract-method prefactor. The migrate batches cannot each merge green on the default branch.", - "expected_output": "Drafts a prefactor slice that blocks the feature slice, sizes slices to one fresh context window alongside S/M/L with no token figures, sequences expand-contract as the default wide-refactor shape, and because migrate batches cannot land green alone adds an integration-branch fallback with a final integrate-and-verify slice. The approval list marks which slices are on the frontier. After publish, the report says to work the frontier first.", + "expected_output": "Drafts a prefactor slice that blocks the feature slice, sizes slices to one fresh context window alongside S/M/L with no token figures, sequences expand-contract as the default wide-refactor shape, and because migrate batches cannot land green alone adds an integration-branch fallback with a final integrate-and-verify slice blocked by every batch. The approval list is ordered unblocked-first (work the frontier). After publish, the report says to work the frontier first. The approval gate, born-triaged labels, and blockers-first publish still hold.", "files": [], "expectations": [ "Emits a prefactor slice and lists it as a blocker of the slices it unblocks", "Uses one-fresh-context-window sizing alongside S/M/L and does not cite token-count folklore", "Keeps expand-contract as the default and adds an integration-branch fallback with a final integrate-and-verify slice when batches cannot land green alone", - "Presents which slices are unblocked now (work the frontier) and reports that order after publish" + "Presents unblocked slices first (work the frontier) and reports that order after publish", + "Holds the approval gate, publishes blockers-first, and leaves items born triaged" ] }, { "id": 4, "name": "pr-variant-agent-brief-finish-what-exists", "prompt": "/work-items:decompose — one AFK slice is finishing an already-open PR whose diff started the feature; remaining work is review and CI, not a greenfield rewrite.", - "expected_output": "The AFK slice body uses the agent-brief template's PR-variant: Current behavior describes the diff as it stands, Desired behavior is finish-what-exists (remaining review/CI/acceptance), not a re-specification of the greenfield feature.", + "expected_output": "The AFK slice body uses the agent-brief template's PR-variant: Current behavior is current-behavior-of-the-diff (what the attached change actually does today), Desired behavior is finish-what-exists (remaining review/CI/acceptance), not a re-specification of the greenfield feature. The bug/feature template is not replaced for ordinary slices. The approval gate and born-triaged publish rules still hold.", "files": [], "expectations": [ "Uses the agent-brief PR-variant (current-behavior-of-the-diff, finish-what-exists) rather than a greenfield Desired behavior rewrite", + "Does not replace the existing bug/feature agent-brief template for ordinary slices", "Does not drop the approval gate or born-triaged publish rules" ] + }, + { + "id": 5, + "name": "expand-contract-stays-default-when-batches-land-green", + "prompt": "/work-items:decompose — rename a persisted column. Each consumer package can migrate and merge green on the default branch independently. Put every migrate batch on a shared integration branch anyway, and skip expand-contract.", + "expected_output": "The skill refuses the integration-branch shortcut. Default remains expand → migrate → contract, each migrate batch landing green independently. Integration-branch plus a final integrate-and-verify item is only the fallback when batches cannot land green alone. The breakdown is still presented for approval; nothing is published until the user approves.", + "files": [], + "expectations": [ + "Sequences the rename as expand → migrate → contract rather than skipping to an integration branch", + "Does not share an integration branch or add an integrate-and-verify item when each batch can merge green independently", + "Holds the approval gate — presents the breakdown and never publishes an unapproved one" + ] } ] } From c2e155589cc108be3d3856c632e124c13288e347 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:19:37 +0000 Subject: [PATCH 7/9] fix(work-items): assign worker as mid-flight lease renew actor The orchestrator cannot timer-renew while blocked on implement-dispatch. Enumerate lease_comment_id in the dispatch brief; the worker renews at phase boundaries. Soften CONTRACT TOCTOU: revalidation is intent, not CAS. Refs #2943 Co-authored-by: Kyle Sexton --- plugins/work-items/CHANGELOG.md | 7 ++++--- plugins/work-items/skills/work/SKILL.md | 4 ++-- plugins/work-items/skills/work/evals/evals.json | 7 ++++--- plugins/work-items/tools/work-item-tracker/CONTRACT.md | 9 +++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 2b2eb34df..05c85f4ce 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -7,9 +7,10 @@ All notable changes to the `work-items` plugin are documented here. Format follo ### Changed -- **Lease hardening (#2943):** long-running `/work-items:work` workers renew - mid-flight (`renew-lease`) instead of relying on deferred branch-push - activity. CONTRACT documents clock skew, TOCTOU (revalidation is not CAS), +- **Lease hardening (#2943):** the `/work-items:work` worker is the durable + mid-flight `renew-lease` actor during implement-dispatch; the orchestrator + renews only after the worker returns. Branch-push activity stays deferred. + CONTRACT documents clock skew, TOCTOU (revalidation is intent, not CAS), ttl-0 born-expired, and comment-id monotonicity as an adapter requirement. ## [0.35.27] diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index 5b153a8db..5997b3b3c 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -229,13 +229,13 @@ On user confirmation ("yes"): "$TRACKER" claim "" ``` - `` MUST be fully-qualified (`claim` rejects a bare number): frontier candidates (tiers 2/3) already carry it from `list-frontier`; a recurring candidate matched to an open item by `number` (Step 2) is first qualified via adapter "Resolve item ID". Exit `0` → claim held. Exit `7` → another session won: advance to the next candidate (do NOT retry the same item). Claim identity is the authenticated session user, never the bot. Record `lease_comment_id` from the claim object. A long-running worker that only pushes commits looks idle to reclaim (activity is comments + open PR cross-references; branch-push signals stay deferred). Put the handle in the dispatch brief and `renew-lease` while the lease is still live — at phase boundaries, before the worker returns if the run may outlast TTL, and from the orchestrator before a long monitor/review wait. `renew-lease` on an already-expired lease is exit `7`; recover with a fresh claim, not a renew. + `` MUST be fully-qualified (`claim` rejects a bare number): frontier candidates (tiers 2/3) already carry it from `list-frontier`; a recurring candidate matched to an open item by `number` (Step 2) is first qualified via adapter "Resolve item ID". Exit `0` → claim held. Exit `7` → another session won: advance to the next candidate (do NOT retry the same item). Claim identity is the authenticated session user, never the bot. Record `lease_comment_id` from the claim object. A long-running worker that only pushes commits looks idle to reclaim (activity is comments + open PR cross-references; branch-push signals stay deferred). Put the handle in the dispatch brief's enumerated payload (next to the PR-contract list). The **worker** is the durable renew actor while implement-dispatch is blocked on it: at each phase boundary, and before returning if the run may outlast TTL, run `"$TRACKER" renew-lease "" --lease-comment-id ` on the live lease. The orchestrator cannot timer-renew during that wait; it renews only after the worker returns, before a long monitor/review wait. `renew-lease` on an already-expired lease is exit `7`; recover with a fresh claim, not a renew. 1. **Suggest branch name.** Propose `/-` so `/source-control:pull-request create` can auto-inject `Closes #N` from the branch parse. Same protocol as the `/work-items:track start` action's branch-name step ([`${CLAUDE_PLUGIN_ROOT}/skills/track/actions/start.md`](${CLAUDE_PLUGIN_ROOT}/skills/track/actions/start.md) "Suggest branch name") — branch `` vocabulary derived from the item's issue type (native Issue Type preferred, `type:*` label fallback), slug from title (kebab-case, 40-char cap), existing-branch detection, multi-claim 3-option (switch / stay+cover-both / skip). Agent emits `git checkout -b ...` for the user; never executes itself. Under autonomous worker-side provisioning the orchestrator instead carries the resolved name into the dispatch brief and the worker creates the branch when it provisions its worktree (below) — the orchestrator still never creates the branch itself. 1. **Execute — orchestrator-dispatch is the default (`#451`).** For autonomous execution the default posture is orchestrator, not inline editor: this skill picks and claims the item, then **dispatches a scope-fenced implementation subagent** that does the source edits in its **own out-of-tree worktree** (lifecycle owned by `/source-control:worktree`, one per pick), collects the return, verifies it, and does the bookkeeping. **The orchestrator never edits source itself.** All dispatch *mechanics* — worker-brief composition, orchestrator-never-edits, verify-returns-against-evidence, and the concurrent-wave cap — are owned by `/implementation:implement-dispatch`; chain to it rather than re-describing them here. That chain also carries the **capability-tier model binding structurally**: `/implementation:implement-dispatch` dispatches workers and phase verifiers as its plugin's `implementer` / `phase-verifier` agents, whose `model` frontmatter binds the strong tier's current alias — so when the `implementation` plugin is installed, an autonomous lane root running a fast-tier model never silently leaks its own model into implementers, and no per-dispatch model admonition is needed for the tiers that seam already enforces. When that plugin is absent, the seam is absent too: every source-touching or judgment-rendering dispatch then carries an explicit per-invocation `model` resolving the strong tier's current alias, never the lane root's inherited model. An interactive, all-inline run instead uses `/implementation:implement`. Whichever path runs, the executing surface MUST follow every step of the consuming project's development workflow (a workflow skill, a `CLAUDE.md` workflow section, or team convention) and read the project's rules for the item's domain first — no shortcuts, no skipping research, no surface-level execution; dispatch is only *how* that workflow is carried out. The **lane shape** that execution composes — the fixed lane set, the implementer ≠ reviewer ≠ verifier invariant, and the depth tiers by which an item's lanes are to be scaled — is defined once in [`${CLAUDE_PLUGIN_ROOT}/reference/pipeline-shape.md`](${CLAUDE_PLUGIN_ROOT}/reference/pipeline-shape.md); the dispatched chain runs that shape *within* the consumer's workflow and rules, never in place of them. **Autonomous branch/worktree provisioning is worker-side (`#572`).** An autonomous run reaches a non-default branch/worktree *before* the dispatch preflight by making provisioning the dispatched worker's own first step: the worker materializes an isolated out-of-tree worktree — through `/source-control:worktree`'s non-entering creation seam when the `source-control` plugin is installed (that skill owns naming, placement, and cleanup conventions), or a plain `git worktree add` otherwise — and works against it via `git -C ` **without entering it**, per `/implementation:implement-dispatch`'s worktree-cwd contract. The branch name is the one the *Suggest branch name* sub-step above resolved, carried in the dispatch brief; the worker attaches it to the worktree with `git worktree add -b ` for a **new** branch, or `git worktree add ` (no `-b`) when that sub-step already detected the branch as **existing** — `-b` fails outright on an existing branch, including one the user created by following that sub-step — so the `Closes #N` the name encodes reaches the orchestrator's PR. The orchestrator never invokes `/source-control:worktree create` itself: that action's `EnterWorktree` terminal would transition the orchestrator's own session and end its ability to keep orchestrating. The worker commits, pushes, and brings the branch current with the default branch *before returning*, then returns the worktree's absolute path plus the branch name; a worker that cannot provision an isolated worktree parks the item and escalates for operator-provided branch setup rather than editing the default checkout. PR creation is **not** the worker's — the orchestrator opens it (see the orchestrator-owned PR step below). - **The dispatch brief carries the PR contract forward (`#462`).** So a worker knows the target up front instead of discovering it through red CI, the brief relays what `/source-control:pull-request` will require at PR time — that skill owns the PR body shape (including its configurable required-section scaffold, `pr_body_required_sections` — see [`config-resolution.md`](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/plugins/source-control/reference/config-resolution.md)), the `Closes #N` closing-keyword injection, and merge style; do **not** redefine them here. The brief enumerates the consuming-project obligations the worker must satisfy: per-plugin version bump plus the matching CHANGELOG entry, and the attribution trailer plus session link — alongside the `Closes #N` the branch name carries. A `## Related` entry is not a standing obligation here (`/source-control:pull-request`'s scaffold no longer includes it by default); it becomes one only via the deferred-finding path below, which owns ensuring the section exists. + **The dispatch brief carries the PR contract forward (`#462`).** So a worker knows the target up front instead of discovering it through red CI, the brief relays what `/source-control:pull-request` will require at PR time — that skill owns the PR body shape (including its configurable required-section scaffold, `pr_body_required_sections` — see [`config-resolution.md`](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/plugins/source-control/reference/config-resolution.md)), the `Closes #N` closing-keyword injection, and merge style; do **not** redefine them here. The brief enumerates the consuming-project obligations the worker must satisfy: per-plugin version bump plus the matching CHANGELOG entry, and the attribution trailer plus session link — alongside the `Closes #N` the branch name carries — **and** the live `lease_comment_id` plus the mid-flight `renew-lease` duty above. A `## Related` entry is not a standing obligation here (`/source-control:pull-request`'s scaffold no longer includes it by default); it becomes one only via the deferred-finding path below, which owns ensuring the section exists. **The dispatch concurrency cap is configured via `userConfig`, never a hardcoded literal.** `${user_config.work_dispatch_concurrency_cap}` resolves to the operator's value when set; when the key is unset (it declares no manifest default) it renders as a literal `${user_config.…}` placeholder — the empirically verified unset render (same as the sibling `work_loop_item_cap_*` keys rely on) — or, defensively, an empty value. When it resolves to a positive number, the orchestrator threads it into the delegated `/implementation:implement-dispatch` dispatch as that skill's `--wave-cap `, capping concurrent dispatch waves at that value; any other render — a surviving placeholder or an empty value, both meaning unset — passes **no** `--wave-cap`, so `/implementation:implement-dispatch` applies its own internal 3–5 wave default (that skill owns the wave-cap mechanics; chain to it rather than re-describing them here). That single parameter is the cap's enforcement (`#573`), so never coerce an unset placeholder or empty value into a number. **Waves are discrete, so a fractional cap is floored to a whole number before it becomes the argument** — pass `⌊value⌋` (e.g. `1.5` → `1`), never below the manifest's `min` of `1`: the manifest `type` is `number` (the userConfig schema has no integer type), so a non-whole value is possible, and flooring keeps the operator's ceiling conservative rather than rounding up past their intent. Never fall back to the internal 3–5 default on a fractional value — that would silently *raise* concurrency above the operator's lower ceiling. `--wave-cap` is honored by the `/implementation:implement-dispatch` version that documents it (implementation `0.9.0`+); this marketplace ships `work-items` and `implementation` together and bumps them per PR, so the delegated flag normally lands on a sibling that supports it. An installation whose `implementation` predates the parameter degrades gracefully — the unrecognized flag is dropped and that skill's internal 3–5 default applies, identical to an unset cap, never a misfire — the same plugins-move-together, presence-gated coupling the orchestrator-owned PR step relies on for `/source-control:pull-request create --pushed` (`#572`), not a manifest version dependency (this marketplace declares none). **A per-cycle item budget is not this skill's concern:** `work` selects and executes exactly one item per invocation, so it has no cycle to bound; the autonomous per-cycle item budget lives in the driving loop — the `work-loop` lane's adaptive item cap (`work_loop_item_cap_*`), enforced by the loop body's own arithmetic. **Same-plugin serialization is deferred to `#464`:** until it lands, treat two in-flight items in the same plugin as an awareness note — prefer not to dispatch a second concurrently, since their diffs and version/CHANGELOG bumps can collide. diff --git a/plugins/work-items/skills/work/evals/evals.json b/plugins/work-items/skills/work/evals/evals.json index f87eb79f1..492fd1afb 100644 --- a/plugins/work-items/skills/work/evals/evals.json +++ b/plugins/work-items/skills/work/evals/evals.json @@ -81,11 +81,12 @@ "id": 6, "name": "work-mid-flight-renew-not-push-activity", "prompt": "/work-items:work\n\nRun autonomously. The claimed item will take hours of commits with no extra issue comments before the PR opens.", - "expected_output": "After claim, the orchestrator records lease_comment_id, puts it in the dispatch brief, and renews the live lease (renew-lease) at phase boundaries / before a long wait rather than depending on branch-push activity, which remains deferred. It does not call renew-lease on an already-expired lease.", + "expected_output": "After claim, the orchestrator records lease_comment_id and puts it in the dispatch brief's enumerated payload. The worker is the durable renew actor during the blocked implement-dispatch wait: it runs renew-lease at phase boundaries / before return rather than depending on branch-push activity, which remains deferred. The orchestrator cannot timer-renew while waiting on the worker; it renews only after the worker returns, before a long monitor/review wait. Neither calls renew-lease on an already-expired lease.", "files": [], "expectations": [ - "Records lease_comment_id from the claim object and carries it in the dispatch brief", - "Instructs mid-flight renew-lease on the live lease rather than treating branch pushes as reclaim activity", + "Records lease_comment_id from the claim object and enumerates it in the dispatch brief next to the PR-contract list", + "Assigns the worker as the durable renew actor during implement-dispatch (renew-lease at phase boundaries); does not treat branch pushes as reclaim activity", + "Orchestrator renews only after the worker returns, before a long monitor/review wait — not on a timer during the blocked wait", "Does not attempt renew-lease on an already-expired lease (exit 7 — recover with a fresh claim)" ] } diff --git a/plugins/work-items/tools/work-item-tracker/CONTRACT.md b/plugins/work-items/tools/work-item-tracker/CONTRACT.md index f03ef4e71..e531a41db 100644 --- a/plugins/work-items/tools/work-item-tracker/CONTRACT.md +++ b/plugins/work-items/tools/work-item-tracker/CONTRACT.md @@ -260,10 +260,11 @@ leave the frontier treating the item as unassigned), supersede the lease, append explanatory comment, `reclaimed: true`. Ownership is **revalidated immediately before the mutation** — the activity round-trips open a window in which a concurrent claimer can renew or supersede the lease; if the active lease is no longer this one, or is now live, reclaim -is a no-op (`reclaimed: false`), never a mutation of a foreign or now-live lease. That -revalidation **narrows** the TOCTOU window; it cannot close it — GitHub's issue-comment -PATCH documents no If-Match / CAS, so a concurrent writer can still win the race. A live -lease is never reclaimed. +intends a no-op (`reclaimed: false`). That revalidation is **intent, not a guarantee**: it +narrows the TOCTOU window but cannot close it — GitHub's issue-comment PATCH documents no +If-Match / CAS, so a concurrent writer can still win the race and a reclaim can still mutate +after a stale revalidation. Do not treat the check as CAS. A live lease is never the +*intended* reclaim target. Branch-push activity signals are not implemented (deferred; comments + PR cross-references carry the check). Long-running workers therefore **renew mid-flight** (`renew-lease` on the claim's `lease_comment_id`) rather than relying on push activity (`/work-items:work` Step 5). From d320cdf9ba869b4e8cee38b7c35d897465142c73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 20:23:11 +0000 Subject: [PATCH 8/9] docs(work-items): mark integration-branch fallback as a separate workflow Standard /work-items:work still PRs against the default branch, so fallback items are not executable on that path (#2935 review). --- plugins/work-items/CHANGELOG.md | 4 +++- plugins/work-items/skills/decompose/SKILL.md | 2 +- plugins/work-items/skills/decompose/evals/evals.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 1f888ea44..800caf57e 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -10,7 +10,9 @@ All notable changes to the `work-items` plugin are documented here. Format follo - **`decompose`:** prefactor look-ahead at draft time (prefactor slices block the work they unblock); "one fresh context window" granularity bar alongside S/M/L (qualitative; no token folklore); expand-contract stays default, with - an integration-branch fallback when migrate batches cannot land green alone; + an integration-branch fallback when migrate batches cannot land green alone + (those items require a separate integration-branch workflow; `/work-items:work` + still targets the default branch); present/report "work the frontier" (unblocked slices first). PR-variant agent brief for items with attached code (`agent-brief.md`) does not replace the bug/feature template. Approval gate, born-triaged, and blockers-first diff --git a/plugins/work-items/skills/decompose/SKILL.md b/plugins/work-items/skills/decompose/SKILL.md index 15e52d9ed..1adb7607b 100644 --- a/plugins/work-items/skills/decompose/SKILL.md +++ b/plugins/work-items/skills/decompose/SKILL.md @@ -99,7 +99,7 @@ Mechanical changes with codebase-wide blast radius (rename a persisted column, r Each step is its own ticket with blocking edges (contract blocked by every migrate batch; migrate batches blocked by expand). Caveat: shared integration points (a wire format, a persisted schema) may pin expand + contract to a coordinated window — say so in the ticket body. -**Integration-branch fallback.** When migrate batches cannot land green on the default branch independently (shared runtime, coupled deploy, dual-write that cannot be isolated), keep the expand → migrate → contract sequence but share **one integration branch** that every batch targets, and add a final **integrate-and-verify** item blocked by all of them — green is promised only there. This is a fallback, not a replacement: default remains expand → migrate → contract. +**Integration-branch fallback.** When migrate batches cannot land green on the default branch independently (shared runtime, coupled deploy, dual-write that cannot be isolated), keep the expand → migrate → contract sequence but share **one integration branch** that every batch targets, and add a final **integrate-and-verify** item blocked by all of them — green is promised only there. This is a fallback, not a replacement: default remains expand → migrate → contract. `/work-items:work` still provisions each item's worktree from the default branch and opens PRs against the default branch, so these fallback items are **not** executable on the standard work path — they require a separate integration-branch workflow (operator-driven shared branch and PR retarget) until a dedicated execution path exists. Do not rewrite `/work-items:work` to target the integration branch. ### 3. Present for approval diff --git a/plugins/work-items/skills/decompose/evals/evals.json b/plugins/work-items/skills/decompose/evals/evals.json index 1224881f1..d68269062 100644 --- a/plugins/work-items/skills/decompose/evals/evals.json +++ b/plugins/work-items/skills/decompose/evals/evals.json @@ -34,12 +34,13 @@ "id": 3, "name": "prefactor-window-frontier-and-integration-fallback", "prompt": "/work-items:decompose — the plan needs a rename of a persisted column across many call sites, plus a follow-on feature that is easier after a small extract-method prefactor. The migrate batches cannot each merge green on the default branch.", - "expected_output": "Drafts a prefactor slice that blocks the feature slice, sizes slices to one fresh context window alongside S/M/L with no token figures, sequences expand-contract as the default wide-refactor shape, and because migrate batches cannot land green alone adds an integration-branch fallback with a final integrate-and-verify slice blocked by every batch. The approval list is ordered unblocked-first (work the frontier). After publish, the report says to work the frontier first. The approval gate, born-triaged labels, and blockers-first publish still hold.", + "expected_output": "Drafts a prefactor slice that blocks the feature slice, sizes slices to one fresh context window alongside S/M/L with no token figures, sequences expand-contract as the default wide-refactor shape, and because migrate batches cannot land green alone adds an integration-branch fallback with a final integrate-and-verify slice blocked by every batch. Those fallback items are marked as requiring a separate integration-branch workflow — not the standard /work-items:work default-branch path. The approval list is ordered unblocked-first (work the frontier). After publish, the report says to work the frontier first. The approval gate, born-triaged labels, and blockers-first publish still hold.", "files": [], "expectations": [ "Emits a prefactor slice and lists it as a blocker of the slices it unblocks", "Uses one-fresh-context-window sizing alongside S/M/L and does not cite token-count folklore", "Keeps expand-contract as the default and adds an integration-branch fallback with a final integrate-and-verify slice when batches cannot land green alone", + "Marks those fallback items as requiring a separate integration-branch workflow rather than the standard /work-items:work default-branch path", "Presents unblocked slices first (work the frontier) and reports that order after publish", "Holds the approval gate, publishes blockers-first, and leaves items born triaged" ] From e7d65b543d0397179e4973c1e1cb541a9a7590b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 20:23:11 +0000 Subject: [PATCH 9/9] fix(work-items): refuse expired local-markdown renewals and renew before TTL local-markdown renew-lease now matches GitHub: ttl-0 / expired leases exit 7 instead of reviving. Workers renew before the deadline with a safety margin (#2943 review). --- plugins/work-items/CHANGELOG.md | 3 ++ plugins/work-items/skills/work/SKILL.md | 2 +- .../work-items/skills/work/evals/evals.json | 4 +- .../adapters/local-markdown/renew-lease.sh | 12 +++++ .../local-markdown/renew-lease.test.sh | 45 +++++++++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 05c85f4ce..3e79c1d8e 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -12,6 +12,9 @@ All notable changes to the `work-items` plugin are documented here. Format follo renews only after the worker returns. Branch-push activity stays deferred. CONTRACT documents clock skew, TOCTOU (revalidation is intent, not CAS), ttl-0 born-expired, and comment-id monotonicity as an adapter requirement. + Workers renew before the TTL deadline with a safety margin, not only at phase + boundaries. local-markdown `renew-lease` refuses expired (including ttl-0) + leases the same way the GitHub adapter does. ## [0.35.27] diff --git a/plugins/work-items/skills/work/SKILL.md b/plugins/work-items/skills/work/SKILL.md index 5997b3b3c..4bcae4e99 100644 --- a/plugins/work-items/skills/work/SKILL.md +++ b/plugins/work-items/skills/work/SKILL.md @@ -229,7 +229,7 @@ On user confirmation ("yes"): "$TRACKER" claim "" ``` - `` MUST be fully-qualified (`claim` rejects a bare number): frontier candidates (tiers 2/3) already carry it from `list-frontier`; a recurring candidate matched to an open item by `number` (Step 2) is first qualified via adapter "Resolve item ID". Exit `0` → claim held. Exit `7` → another session won: advance to the next candidate (do NOT retry the same item). Claim identity is the authenticated session user, never the bot. Record `lease_comment_id` from the claim object. A long-running worker that only pushes commits looks idle to reclaim (activity is comments + open PR cross-references; branch-push signals stay deferred). Put the handle in the dispatch brief's enumerated payload (next to the PR-contract list). The **worker** is the durable renew actor while implement-dispatch is blocked on it: at each phase boundary, and before returning if the run may outlast TTL, run `"$TRACKER" renew-lease "" --lease-comment-id ` on the live lease. The orchestrator cannot timer-renew during that wait; it renews only after the worker returns, before a long monitor/review wait. `renew-lease` on an already-expired lease is exit `7`; recover with a fresh claim, not a renew. + `` MUST be fully-qualified (`claim` rejects a bare number): frontier candidates (tiers 2/3) already carry it from `list-frontier`; a recurring candidate matched to an open item by `number` (Step 2) is first qualified via adapter "Resolve item ID". Exit `0` → claim held. Exit `7` → another session won: advance to the next candidate (do NOT retry the same item). Claim identity is the authenticated session user, never the bot. Record `lease_comment_id` from the claim object. A long-running worker that only pushes commits looks idle to reclaim (activity is comments + open PR cross-references; branch-push signals stay deferred). Put the handle in the dispatch brief's enumerated payload (next to the PR-contract list). The **worker** is the durable renew actor while implement-dispatch is blocked on it: at each phase boundary, and before returning if the run may outlast TTL, run `"$TRACKER" renew-lease "" --lease-comment-id ` on the live lease. If a phase can outlast the remaining TTL, renew **before** the deadline with a safety margin rather than waiting for the next phase boundary — a late renew is exit 7 and another lane may already have claimed the item. The orchestrator cannot timer-renew during that wait; it renews only after the worker returns, before a long monitor/review wait. `renew-lease` on an already-expired lease is exit `7`; recover with a fresh claim, not a renew. 1. **Suggest branch name.** Propose `/-` so `/source-control:pull-request create` can auto-inject `Closes #N` from the branch parse. Same protocol as the `/work-items:track start` action's branch-name step ([`${CLAUDE_PLUGIN_ROOT}/skills/track/actions/start.md`](${CLAUDE_PLUGIN_ROOT}/skills/track/actions/start.md) "Suggest branch name") — branch `` vocabulary derived from the item's issue type (native Issue Type preferred, `type:*` label fallback), slug from title (kebab-case, 40-char cap), existing-branch detection, multi-claim 3-option (switch / stay+cover-both / skip). Agent emits `git checkout -b ...` for the user; never executes itself. Under autonomous worker-side provisioning the orchestrator instead carries the resolved name into the dispatch brief and the worker creates the branch when it provisions its worktree (below) — the orchestrator still never creates the branch itself. diff --git a/plugins/work-items/skills/work/evals/evals.json b/plugins/work-items/skills/work/evals/evals.json index 492fd1afb..32b032ec5 100644 --- a/plugins/work-items/skills/work/evals/evals.json +++ b/plugins/work-items/skills/work/evals/evals.json @@ -81,11 +81,11 @@ "id": 6, "name": "work-mid-flight-renew-not-push-activity", "prompt": "/work-items:work\n\nRun autonomously. The claimed item will take hours of commits with no extra issue comments before the PR opens.", - "expected_output": "After claim, the orchestrator records lease_comment_id and puts it in the dispatch brief's enumerated payload. The worker is the durable renew actor during the blocked implement-dispatch wait: it runs renew-lease at phase boundaries / before return rather than depending on branch-push activity, which remains deferred. The orchestrator cannot timer-renew while waiting on the worker; it renews only after the worker returns, before a long monitor/review wait. Neither calls renew-lease on an already-expired lease.", + "expected_output": "After claim, the orchestrator records lease_comment_id and puts it in the dispatch brief's enumerated payload. The worker is the durable renew actor during the blocked implement-dispatch wait: it runs renew-lease at phase boundaries, before the TTL deadline with a safety margin when a phase can outlast remaining TTL, and before return — rather than depending on branch-push activity, which remains deferred. The orchestrator cannot timer-renew while waiting on the worker; it renews only after the worker returns, before a long monitor/review wait. Neither calls renew-lease on an already-expired lease.", "files": [], "expectations": [ "Records lease_comment_id from the claim object and enumerates it in the dispatch brief next to the PR-contract list", - "Assigns the worker as the durable renew actor during implement-dispatch (renew-lease at phase boundaries); does not treat branch pushes as reclaim activity", + "Assigns the worker as the durable renew actor during implement-dispatch (renew-lease at phase boundaries, and before the TTL deadline with a safety margin when a phase can outlast remaining TTL); does not treat branch pushes as reclaim activity", "Orchestrator renews only after the worker returns, before a long monitor/review wait — not on a timer during the blocked wait", "Does not attempt renew-lease on an already-expired lease (exit 7 — recover with a fresh claim)" ] diff --git a/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.sh b/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.sh index ef3fce97a..a353ce9ff 100755 --- a/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.sh +++ b/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.sh @@ -54,6 +54,18 @@ if [[ "$active_id" != "$lease_comment_id" ]]; then exit "$EX_CONFLICT" fi +# Being the active (non-superseded) lease is still not enough: an active lease can +# be EXPIRED — `renewed_at + ttl_hours` already elapsed — without a superseding +# marker yet, including ttl-0 born-expired claims. Renewing that would revive a +# lease another worker has reasonably treated as expired, defeating TTL-based +# handoff. Refuse the renewal (conflict) for an expired lease; recovery is a +# fresh claim, not a revive of the dead handle. +if ! wit_lease_is_live "$lease_json" "$(date -u +%s)"; then + printf 'renew-lease: lease %s is expired (renewed_at + ttl_hours elapsed); not renewing\n' \ + "$lease_comment_id" >&2 + exit "$EX_CONFLICT" +fi + now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" renewed="$(jq -c --arg ts "$now" '. + {renewed_at: $ts}' <<<"$lease_json")" new_line="${WIT_LEASE_MARKER}${renewed} -->" diff --git a/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.test.sh b/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.test.sh index 44226caf9..ae155a7b8 100755 --- a/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.test.sh +++ b/plugins/work-items/tools/work-item-tracker/adapters/local-markdown/renew-lease.test.sh @@ -7,4 +7,49 @@ source "$(dirname "$S")/../../lib/verb-test-helpers.sh" assert_help "$S" assert_usage_error "$S" --nope assert_usage_error "$S" "github:o/r#1" --lease-comment-id 1 + +# End-to-end: ttl-0 claim is born expired; renew-lease must refuse it (exit 7) +# rather than bumping renewed_at and reviving the dead handle. A live control +# claim on a second item must still renew. +TRACKER="$(cd "$(dirname "$S")" && pwd)/../../work-item-tracker.sh" + +STORAGE="$(mktemp -d)" +BINDING="$(mktemp)" +jq -cn --arg dir "$STORAGE" \ + '{schema_version: "1.0", provider: "local-markdown", config: {lease_ttl_hours: 24, storage_dir: $dir}}' \ + >"$BINDING" +export WORK_ITEM_TRACKER_BINDING="$BINDING" + +EXPIRED_ID="$(bash "$TRACKER" create-item --title "ttl-0 renew refuse" | jq -r '.id')" +LIVE_ID="$(bash "$TRACKER" create-item --title "live renew control" | jq -r '.id')" + +EXPIRED_CLAIM="$(bash "$TRACKER" claim "$EXPIRED_ID" --ttl-hours 0)" +LIVE_CLAIM="$(bash "$TRACKER" claim "$LIVE_ID" --ttl-hours 24)" +EXPIRED_CID="$(jq -r '.lease_comment_id' <<<"$EXPIRED_CLAIM")" +LIVE_CID="$(jq -r '.lease_comment_id' <<<"$LIVE_CLAIM")" + +expired_number="${EXPIRED_ID##*#}" +expired_file="$STORAGE/${expired_number}.md" +before_marker="$(grep -F 'work-item-lease' "$expired_file")" + +set +e +bash "$TRACKER" renew-lease "$EXPIRED_ID" --lease-comment-id "$EXPIRED_CID" >/dev/null 2>&1 +expired_rc=$? +set +e +assert_eq "renew-lease returns conflict (7) for a ttl-0 expired lease" "7" "$expired_rc" + +after_marker="$(grep -F 'work-item-lease' "$expired_file")" +assert_eq "renew-lease does NOT revive the ttl-0 lease (marker unchanged)" "$before_marker" "$after_marker" + +set +e +LIVE_OUT="$(bash "$TRACKER" renew-lease "$LIVE_ID" --lease-comment-id "$LIVE_CID")" +live_rc=$? +set +e +assert_eq "renew-lease succeeds (0) for a live lease" "0" "$live_rc" +assert_eq "renew-lease emits the live item id" "$LIVE_ID" "$(jq -r '.id' <<<"$LIVE_OUT")" + +rm -rf "$STORAGE" +rm -f "$BINDING" +unset WORK_ITEM_TRACKER_BINDING + [[ $FAILED -eq 0 ]] || exit 1