Skip to content

fix: surface newly ready gated backlog work - #48

Merged
knowttl merged 7 commits into
mainfrom
fm/fm-ready-work-monitor-backstop
Sep 23, 2026
Merged

knowttl merged 7 commits into
mainfrom
fm/fm-ready-work-monitor-backstop

Conversation

@knowttl

@knowttl knowttl commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Intent

please create a proper fix based on the stall scout finding.

please put them as teo separate pull requests.

also ensure that the fixes generalize across the firstmate repo.

also ensure that before you create a pull request on the upstream repo please also ensure we properly follow the upstream repo contribution rules if there are any and the standards it has defined?

Context the ask refers to: a scout investigated why a remote second mate stalled on dependency-gated follow-on work.
Its report found two gaps and the captain asked for them as two separate pull requests.
The first (a wording fix to AGENTS.md section 10 and the second-mate charter so authorized gated phases are filed as backlog items at once) is a separate task.
This task is the second pull request: the mechanical monitoring gap.
In substance, from the report: queued work is re-evaluated only at this home's own teardown (a printed reminder to run ready) and at session start.
The watcher heartbeat wakes only on an unsurfaced actionable status, never on a backlog item that has newly become ready, and supervision need counts in-flight metadata, sources, checks, and Relay but not queued, held, or date-gated items, so an otherwise idle home runs no watcher at all.
So a captain hold with --until whose date passes, or a blocker cleared by anything other than this home's own teardown (a captain answer or release, a manual tasks-axi done, cross-home work), is not noticed until the next teardown or session start, even though the hold feature promises the item "resurfaces on its date".
The proposed fix: surface newly ready backlog items (date gate passed, or every blocker closed) as a wake from the watcher's routine heartbeat, and count date-gated or blocked queued items as supervision need so an idle home keeps a watcher while such items exist; optionally, have teardown list the dependents the closed item just unblocked instead of only telling the agent to run ready.

What Changed

  • Add a ready-work backstop that queues a durable wake once per readiness transition when a queued item’s date gate passes or its blockers close.
  • Run readiness checks from the watcher heartbeat and supported backlog transitions, including captain answers and teardown.
  • Count live-gated queued work as supervision need so idle homes keep a watcher; update guard messages, documentation, and tests.

Risk Assessment

⚠️ Medium: The change spans watcher, supervision, backlog mutation, and teardown paths, but the full review found no new material defect beyond limitations already accepted in earlier rounds.

Testing

The baseline ready-work check passed; after the lock fix, the ready-work, guard, teardown, and captain-hold checks passed. The final watcher check used an isolated real tmux server. No graphical screenshot was produced because the changed product surface is CLI output and persisted wake state.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
A dated captain hold becomes due and its queued work receives one wake ✅ pass live tests/fm-ready-work.test.sh exercises a real tasks-axi backlog and confirms one durable wake when a dated hold becomes due.
Closing a blocker while idle wakes its newly ready dependent once ✅ pass live tests/fm-ready-work.test.sh closes a blocker through the supported task wrapper and checks the dependent wake and deduplication.
Answering a captain hold releases dependent work with a durable wake ✅ pass live tests/fm-captain-hold-lifecycle.test.sh answers a captain hold and checks for exactly one persisted dependent wake.
The running watcher detects an externally cleared blocker and does not repeat the wake ✅ pass live tests/fm-ready-work.test.sh runs the watcher against an isolated real tmux server and checks its output and wake queue.
An idle home keeps supervision for a future date without keeping it for an undated hold ✅ pass live tests/fm-ready-work.test.sh and tests/fm-turnend-guard.test.sh check live supervision need for dated gates and no continuing need for undated holds.
Teardown closure wakes work that the closed item unblocked ✅ pass live tests/fm-teardown.test.sh checks the wake queued when teardown closes a blocking item.

Known limit

A bare tasks-axi mutation that bypasses bin/fm-tasks-axi.sh (or a hand edit of the backlog) is an unsupported path: AGENTS.md routes every backlog read and mutation through that wrapper, which now surfaces work its close, release, or unblock made ready.
Backlog blockers are home-local, so such a missed transition is picked up by the watcher's next ready-work scan when a watcher is running, and otherwise by the next session start.
No permanent watcher is kept for work blocked only by an undated captain hold or by queued work this home has not dispatched; date gates keep the watcher until their due date.

Upstream applicability

Upstream kunchenguid/firstmate main (52fca51) has the same gap: bin/fm-supervision-lib.sh counts only in-flight metadata, process-event sources, registered checks, and Relay, bin/fm-watch.sh never reads the backlog, and teardown only prints a reminder to run ready.
Upstream's bin/fm-watch.sh has diverged from this fork, so an upstream port needs a rebase of the watcher hunk and must be raised through no-mistakes per upstream CONTRIBUTING.md.

Harness and runtime-backend compatibility review

  • Claude Code: the Stop auto-arm (bin/fm-claude-stop-autoarm.sh) and the Stop turn-end guard both read the shared supervision predicate, so live-gated queued work arms or demands a watcher.
  • Codex, Grok, OpenCode, Pi, pi-signed, and omp primaries call the shared bin/fm-turnend-guard.sh; Cursor's bin/fm-turnend-guard-cursor.sh calls fm_supervision_needed. All inherit the new need through the one predicate, and bin/fm-guard.sh names it in its watcher-down banner.
  • Kimi primaries have no project-level hook (docs/turnend-guard.md "Compatibility limits"), so they get the wake but no turn-end enforcement, as for every other need today.
  • OpenCode's watch-arm plugin keeps its own pre-existing shouldArm (in-flight metadata and Relay only, not sources, checks, or gated work); its turn-end guard still enforces the shared need. That drift predates this change and is left for a follow-up.
  • Pi supervision branch: the new wake is a check: row, which stays main-only while attended and is offered to the branch under the away posture, like every other check row.
  • Away-mode daemon (non-Pi): the wake is queued in every posture and classify_check escalates it.
  • Runtime backends (tmux, Herdr, zellij, Orca, cmux): not applicable - the scan reads only the backlog through bin/fm-tasks-axi.sh, never a pane or endpoint.
  • Secondmate homes, local and remote: each runs its own watcher, wrapper, and turn-end guard against its own home's backlog; a home without tasks-axi or with config/backlog-backend=manual steps aside rather than blocking.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (3) ✅
  • 🚨 bin/fm-ready-work.sh:127 - The intent requires counting blocked queued items as supervision need. Here, a dependent blocked by an undated captain hold or a queued ready item is excluded; the same exclusion appears at bin/fm-ready-work.sh:128 and in the supervision verdict at bin/fm-supervision-lib.sh:92. If a captain answer releases the hold or a manual command closes the blocker while the home is idle, no watcher is running to surface the dependent. The diff narrows the stated criterion to blockers classified as live-gated; restore supervision for these blocked items.
  • 🚨 bin/fm-ready-work.sh:207 - With no .ready-work-surfaced record, this branch silently treats the current ready set as surfaced. A date or blocker that clears before the watcher's first scan therefore gets no wake. The same suppression occurs in the optional teardown path: bin/fm-ready-work.sh:256 commits before it prints, and bin/fm-teardown.sh:1562 captures that output, so an interrupted delivery can leave the item marked surfaced. The silent seed and teardown suppression are not required by the intent; remove them so an unreported ready item remains eligible for the watcher.
  • ⚠️ bin/fm-ready-work.sh:151 - The scanner derives the home from the state directory even when FM_STATE_OVERRIDE points elsewhere. That override is supported by the watcher at bin/fm-watch.sh:141 and both supervision callers at bin/fm-guard.sh:47 and bin/fm-turnend-guard.sh:97. With an alternate state directory and the normal data directory, scans read the wrong home's backlog or return zero supervision need. Use the established FM_HOME and directory overrides for these reads.
  • ⚠️ bin/fm-ready-work.sh:117 - The scan also wakes for a newly filed task that was ready from creation. The intent specifies transitions caused by a date passing or blockers closing, and does not require this additional wake path. Narrow the matching path to those gate-clearing transitions.
  • ⚠️ bin/fm-watch.sh:239 - FM_READY_SCAN introduces a separate cadence option. The intent calls for scanning on the routine heartbeat cadence and gives no requirement for a configurable second schedule. Remove the option and use the base heartbeat interval.

🔧 Fix applied.
6 issues (3 errors, 3 warnings) still open:

  • 🚨 bin/fm-ready-work.sh:127 - The intent requires counting blocked queued items as supervision need. Here, a dependent blocked by an undated captain hold or a queued ready item is excluded; the same exclusion appears at bin/fm-ready-work.sh:128 and in the supervision verdict at bin/fm-supervision-lib.sh:92. If a captain answer releases the hold or a manual command closes the blocker while the home is idle, no watcher is running to surface the dependent. The diff narrows the stated criterion to blockers classified as live-gated; restore supervision for these blocked items.
  • 🚨 bin/fm-ready-work.sh:207 - With no .ready-work-surfaced record, this branch silently treats the current ready set as surfaced. A date or blocker that clears before the watcher's first scan therefore gets no wake. The same suppression occurs in the optional teardown path: bin/fm-ready-work.sh:256 commits before it prints, and bin/fm-teardown.sh:1562 captures that output, so an interrupted delivery can leave the item marked surfaced. The silent seed and teardown suppression are not required by the intent; remove them so an unreported ready item remains eligible for the watcher.
  • ⚠️ bin/fm-ready-work.sh:151 - The scanner derives the home from the state directory even when FM_STATE_OVERRIDE points elsewhere. That override is supported by the watcher at bin/fm-watch.sh:141 and both supervision callers at bin/fm-guard.sh:47 and bin/fm-turnend-guard.sh:97. With an alternate state directory and the normal data directory, scans read the wrong home's backlog or return zero supervision need. Use the established FM_HOME and directory overrides for these reads.
  • ⚠️ bin/fm-ready-work.sh:117 - The scan also wakes for a newly filed task that was ready from creation. The intent specifies transitions caused by a date passing or blockers closing, and does not require this additional wake path. Narrow the matching path to those gate-clearing transitions.
  • 🚨 bin/fm-ready-work.sh:128 - The fix round added wake calls to fm-tasks-axi.sh and fm-captain-hold.sh, but left the stated bare manual tasks-axi done path uncovered. If an idle home has a dependent blocked only by a queued item, line 128 excludes it from supervision; closing that blocker directly produces no wake, and no watcher is running to scan it. The same gap applies to an external or cross-home close of such a blocker. The intent requires noticing blocker clears outside teardown, while the fix-round instruction forbids a permanent watcher for this case. Resolve which mechanism should cover bare external closes; wrapper hooks cannot intercept them.
  • ⚠️ bin/fm-ready-work.sh:239 - The new surface mode is an additional output and commit path that no production caller uses. The intent requires a durable wake, and the optional teardown behavior uses wake. Remove surface and its test-only callers; this also avoids marking work surfaced merely because printing to stdout succeeded.

🔧 Fix applied.
7 issues (3 errors, 4 warnings) still open:

  • 🚨 bin/fm-ready-work.sh:127 - The intent requires counting blocked queued items as supervision need. Here, a dependent blocked by an undated captain hold or a queued ready item is excluded; the same exclusion appears at bin/fm-ready-work.sh:128 and in the supervision verdict at bin/fm-supervision-lib.sh:92. If a captain answer releases the hold or a manual command closes the blocker while the home is idle, no watcher is running to surface the dependent. The diff narrows the stated criterion to blockers classified as live-gated; restore supervision for these blocked items.
  • 🚨 bin/fm-ready-work.sh:207 - With no .ready-work-surfaced record, this branch silently treats the current ready set as surfaced. A date or blocker that clears before the watcher's first scan therefore gets no wake. The same suppression occurs in the optional teardown path: bin/fm-ready-work.sh:256 commits before it prints, and bin/fm-teardown.sh:1562 captures that output, so an interrupted delivery can leave the item marked surfaced. The silent seed and teardown suppression are not required by the intent; remove them so an unreported ready item remains eligible for the watcher.
  • ⚠️ bin/fm-ready-work.sh:151 - The scanner derives the home from the state directory even when FM_STATE_OVERRIDE points elsewhere. That override is supported by the watcher at bin/fm-watch.sh:141 and both supervision callers at bin/fm-guard.sh:47 and bin/fm-turnend-guard.sh:97. With an alternate state directory and the normal data directory, scans read the wrong home's backlog or return zero supervision need. Use the established FM_HOME and directory overrides for these reads.
  • ⚠️ bin/fm-ready-work.sh:117 - The scan also wakes for a newly filed task that was ready from creation. The intent specifies transitions caused by a date passing or blockers closing, and does not require this additional wake path. Narrow the matching path to those gate-clearing transitions.
  • 🚨 bin/fm-ready-work.sh:128 - The fix round added wake calls to fm-tasks-axi.sh and fm-captain-hold.sh, but left the stated bare manual tasks-axi done path uncovered. If an idle home has a dependent blocked only by a queued item, line 128 excludes it from supervision; closing that blocker directly produces no wake, and no watcher is running to scan it. The same gap applies to an external or cross-home close of such a blocker. The intent requires noticing blocker clears outside teardown, while the fix-round instruction forbids a permanent watcher for this case. Resolve which mechanism should cover bare external closes; wrapper hooks cannot intercept them.
  • ⚠️ bin/fm-ready-work.sh:239 - The new surface mode is an additional output and commit path that no production caller uses. The intent requires a durable wake, and the optional teardown behavior uses wake. Remove surface and its test-only callers; this also avoids marking work surfaced merely because printing to stdout succeeded.
  • ⚠️ bin/fm-ready-work.sh:166 - The new FM_READY_WORK_TIMEOUT option controls both backlog reads and lock waits (also at bin/fm-ready-work.sh:197). The requested heartbeat backstop needs bounded operations, but no intent requirement needs a configurable timeout. Remove the option and use a fixed bound.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
A dated captain hold becomes due and its queued work receives one wake ✅ pass live tests/fm-ready-work.test.sh exercises a real tasks-axi backlog and confirms one durable wake when a dated hold becomes due.
Closing a blocker while idle wakes its newly ready dependent once ✅ pass live tests/fm-ready-work.test.sh closes a blocker through the supported task wrapper and checks the dependent wake and deduplication.
Answering a captain hold releases dependent work with a durable wake ✅ pass live tests/fm-captain-hold-lifecycle.test.sh answers a captain hold and checks for exactly one persisted dependent wake.
The running watcher detects an externally cleared blocker and does not repeat the wake ✅ pass live tests/fm-ready-work.test.sh runs the watcher against an isolated real tmux server and checks its output and wake queue.
An idle home keeps supervision for a future date without keeping it for an undated hold ✅ pass live tests/fm-ready-work.test.sh and tests/fm-turnend-guard.test.sh check live supervision need for dated gates and no continuing need for undated holds.
Teardown closure wakes work that the closed item unblocked ✅ pass live tests/fm-teardown.test.sh checks the wake queued when teardown closes a blocking item.
  • tests/fm-ready-work.test.sh before the fix and after the final test change
  • tests/fm-turnend-guard.test.sh
  • tests/fm-teardown.test.sh
  • tests/fm-captain-hold-lifecycle.test.sh
  • git diff --check
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

Queued backlog work gated on a hold date or on blockers could become
ready without any turn in the home - a date passing, or a blocker closed
by a captain answer, a hand-run tasks-axi done, or work elsewhere - and
nothing noticed until the next teardown or session start.

bin/fm-ready-work.sh owns the backstop: the watcher runs a ready-work
scan on the base heartbeat cadence and wakes once per readiness
transition, teardown names the work its own close unblocked and records
it as surfaced, and live-gated queued work (a future hold date, or
blockers that are in flight or live-gated themselves) now counts as
supervision need while undated holds never keep a watcher alive.
…e-library load and stopping ShellCheck from recursively analyzing the new ready-work import through watcher and supervision callers. The ready-work script remains linted as its own root. Local lint, source-aware checks, and ready-work behavior tests pass
@knowttl
knowttl merged commit 1c7f81e into main Sep 23, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant