fix: classify captain holds from structured state - #3508
Merged
Conversation
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the reviewed captain-hold classification and projection paths. No blocking failure remains. Reviews (17): Last reviewed commit: "no-mistakes(ci): Updated the stock macOS..." | Re-trigger Greptile |
kunchenguid
force-pushed
the
fm/fm-bearings-undated-hold-aging-r1
branch
from
September 2, 2026 09:04
32fe314 to
e889ca0
Compare
kunchenguid
force-pushed
the
fm/fm-bearings-undated-hold-aging-r1
branch
from
September 2, 2026 09:49
e889ca0 to
ce9453b
Compare
kunchenguid
force-pushed
the
fm/fm-bearings-undated-hold-aging-r1
branch
from
September 3, 2026 22:43
95696f1 to
3c8da5d
Compare
kunchenguid
force-pushed
the
fm/fm-bearings-undated-hold-aging-r1
branch
from
September 4, 2026 00:01
8c7437f to
998ed7c
Compare
kunchenguid
force-pushed
the
fm/fm-bearings-undated-hold-aging-r1
branch
from
September 4, 2026 04:14
998ed7c to
9dcbe8c
Compare
Bearings was treating undated parked-style holds as live calls; mark those phrasings deferred and project holds older than a configurable 14-day since date as Charted Next gates instead.
…shot, 45 Bearings). Prevented fresh holds on old tasks from aging via stale `since` dates by aging only stamped holds. Added behavioral regressions and verified both suites plus Bash 3.2 parsing
…est count to 51. Preserved the v1 summary schema for compatibility while rejecting hold-bearing summaries missing the new aging fields, preventing stale caches from restoring noisy calls. Verified fleet snapshot, Bearings snapshot (51 tests), home-summary refresh, secondmate reconciliation, Bash 3.2 parsing, and diff checks
…w reveals deferred/aged captain holds even when blocked, for both main and secondmate homes, and removes their duplicate Charted Next gates. Added behavioral regression coverage and updated documentation. The prose-classifier finding was not applied because exact complete-phrase matching is explicitly required by the author intent; contextual wording remains live. Verified with Bearings and fleet snapshot tests, `bin/fm-lint.sh`, Bash syntax checking, and `git diff --check`
…ed parked-style hold is live only when it is not explicitly non-actionable, so blocked due holds remain gated by default and are revealed by --all-decisions. Added behavioral regression coverage for that case. Preserved complete-reason parked-style classification as required by the author intent. Verified with tests/fm-bearings-snapshot.test.sh, bin/fm-lint.sh, and git diff --check
Under --all-decisions a deferred hold is revealed and its Charted Next gate is removed, but the revealed row carried only the bare hold reason. A date-deferred or blocked hold therefore read exactly like a genuine live decision, because the until date, the age, and the blocking work only ever appeared on the gate row that the reveal replaces. Annotate a row that is revealed because it is deferred with the same vocabulary the gate uses - until <date>, held <n>d, and the blocking work - so the expanded view reads as deferred-but-shown. A genuinely live call is left unannotated, and the default board is unchanged.
Bucket membership was decided by several independent expressions, and two of them matched hold reason or body prose. That produced a recurring class of defects: holds that fell through every bucket and vanished from the board, and live decisions silently suppressed because their wording happened to contain a marker word - a reason of "non-deferred release choice" matched DEFERRED and disappeared. Replace all of it with one total classifier over structured fields only: hold_kind, state, hold_until, unresolved_blocker_ids, and the machine-written hold-set timestamp. Every captain hold gets exactly one hold_bucket - blocked, dated, aged, or live - so no hold can fall through and none can match two. captain_actionable is exactly the live bucket, and the --all-decisions reveal is a property of the bucket rather than a second filter. No hold reason or body prose is matched anywhere in the projection, so wording can no longer hide, reveal, or reclassify a decision. A hold that is superseded or no longer required is closed through the hold lifecycle instead of lingering as an open hold flagged by a keyword.
kunchenguid
force-pushed
the
fm/fm-bearings-undated-hold-aging-r1
branch
from
September 5, 2026 00:57
c2a78eb to
8333797
Compare
… from 17 to 18 tests. Verified the suite under Bash 3.2.57: all 18 tests pass. `git diff --check` also passes
…to 53 Bearings tests. Verified all 53 pass under Bash 3.2.57; git diff --check passes
lytv
pushed a commit
to lytv/mymate
that referenced
this pull request
Sep 8, 2026
* Keep parked and aged undated captain holds off live Captain's Call. Bearings was treating undated parked-style holds as live calls; mark those phrasings deferred and project holds older than a configurable 14-day since date as Charted Next gates instead. * no-mistakes(review): Bound parked marker matching to lexical tokens * no-mistakes(review): Age undated holds from durable hold-set dates * no-mistakes(review): Reset re-held timestamps and scan full bodies * no-mistakes(review): Preserve timestamp precision and prioritize parked suppression * no-mistakes(document): Document undated captain-hold aging * no-mistakes(ci): Fixed stock Bash CI test-count expectations (16 snapshot, 45 Bearings). Prevented fresh holds on old tasks from aging via stale `since` dates by aging only stamped holds. Added behavioral regressions and verified both suites plus Bash 3.2 parsing * no-mistakes(ci): account for rebased snapshot regression * no-mistakes(review): Restore legacy hold aging and mandate wrapper * no-mistakes(review): Restrict hold stamps to canonical leading lines * no-mistakes(review): Exclude historical answers and deduplicate revealed holds * no-mistakes(document): Correct captain-hold projection documentation * no-mistakes(ci): Rebased onto 8988af2 and resolved Bearings conflicts. Fixed the hold timestamp race by persisting and verifying the timestamp before publishing the captain hold; failures now leave the task unheld. Added behavioral coverage for ordering and failure handling. Preserved the required parked-phrase projection behavior. Relevant snapshot, Bearings, lifecycle, syntax, and ShellCheck validations pass * no-mistakes(review): Bound current prose before historical resolutions * no-mistakes(review): Preserve hold age across interrupted answers * no-mistakes(review): Preserve leading hold stamps until answer closure * no-mistakes(review): Normalize answer bodies on matching retries * no-mistakes(review): Document concurrent re-hold age-basis limitation * no-mistakes(document): Refresh captain hold lifecycle documentation * no-mistakes(ci): Fixed both CI failures. Updated the macOS Bash snapshot expectation from 45 to 46 Bearings tests. Narrowed parked-style deferral matching to explicit hold-reason prefixes while preserving legacy explicit markers and preventing contextual prose from hiding active decisions. Added behavioral regression coverage. Verified with stock Bash 3.2: 17 fleet snapshot tests and 46 Bearings tests pass; full lint and workflow validation also pass * no-mistakes(ci): Fixed Greptile’s P1 finding by restricting parked-style deferral phrases to complete hold-reason markers. Contextual reasons beginning with “not urgent,” “queued opportunity,” or “captain-gated” now remain visible decisions. Added behavioral coverage through the real fleet and Bearings snapshot paths and updated documentation. Verified both snapshot suites under Bash 3.2 (17 fleet tests and 46 Bearings tests), syntax checks, and git diff checks. The no-mistakes attestation failure is external/stale and requires the outer pipeline to refresh it for the new head * no-mistakes(ci): Fixed parked-style undated captain holds disappearing from the default Bearings board. They now project to Charted Next with omitted[] disclosure, while --all-decisions reveals them and removes the safety gate. Added behavioral coverage for the reported “not urgent” case and aligned documentation. Verified fm-bearings-snapshot, fleet snapshot view, and captain-hold lifecycle tests; shellcheck, bash syntax, and git diff checks pass * no-mistakes(test): Stabilize concurrency budget and provision timeout tests * no-mistakes(document): Correct captain hold documentation details * no-mistakes(ci): Fixed hold-reason parsing so commas in contextual reasons are preserved and do not incorrectly defer live Captain's Call decisions. Added end-to-end fleet/Bearings regression coverage. Reworked the flaky Herdr timeout test to assert observable late-launch behavior rather than process-ID liveness. Verified both snapshot suites, Herdr test 5 consecutive times, shell syntax, shellcheck, and git diff checks * Restore the Herdr lab timeout test to its main version. The stabilization rounds reworked tests/fm-herdr-lab.test.sh while chasing a load-induced flake, replacing the fake server's wall-clock delay with a SIGSTOP'd process and asserting that the blocked process is gone after a timed-out provision. A stopped process does not die from SIGTERM, so that assertion fails on Linux and the portable parallel shard stayed red. That test is unrelated to the undated captain-hold projection this branch delivers and was identical to main before these rounds, so restore main's version exactly. It still proves that a timed-out provision cancels its late launch before teardown. * no-mistakes(review): Preserve metadata-like prose in captain hold reasons * no-mistakes(review): Resurface due dated captain holds * no-mistakes(review): Distinguish parked holds from explicit deferrals * no-mistakes(review): Invalidate legacy secondmate summary caches * no-mistakes(review): Keep blocked deferred holds in Charted Next * no-mistakes(review): Count blocked deferred holds in omission disclosure * no-mistakes(document): Correct captain-hold projection documentation * no-mistakes(ci): Fixed both CI failures. Updated the macOS Bearings test count to 51. Preserved the v1 summary schema for compatibility while rejecting hold-bearing summaries missing the new aging fields, preventing stale caches from restoring noisy calls. Verified fleet snapshot, Bearings snapshot (51 tests), home-summary refresh, secondmate reconciliation, Bash 3.2 parsing, and diff checks * no-mistakes(ci): Fixed Greptile’s valid finding: `--all-decisions` now reveals deferred/aged captain holds even when blocked, for both main and secondmate homes, and removes their duplicate Charted Next gates. Added behavioral regression coverage and updated documentation. The prose-classifier finding was not applied because exact complete-phrase matching is explicitly required by the author intent; contextual wording remains live. Verified with Bearings and fleet snapshot tests, `bin/fm-lint.sh`, Bash syntax checking, and `git diff --check` * no-mistakes(ci): Fixed the actionable-state bug in Bearings: an arrived parked-style hold is live only when it is not explicitly non-actionable, so blocked due holds remain gated by default and are revealed by --all-decisions. Added behavioral regression coverage for that case. Preserved complete-reason parked-style classification as required by the author intent. Verified with tests/fm-bearings-snapshot.test.sh, bin/fm-lint.sh, and git diff --check * Show why a revealed captain hold is deferred. Under --all-decisions a deferred hold is revealed and its Charted Next gate is removed, but the revealed row carried only the bare hold reason. A date-deferred or blocked hold therefore read exactly like a genuine live decision, because the until date, the age, and the blocking work only ever appeared on the gate row that the reveal replaces. Annotate a row that is revealed because it is deferred with the same vocabulary the gate uses - until <date>, held <n>d, and the blocking work - so the expanded view reads as deferred-but-shown. A genuinely live call is left unannotated, and the default board is unchanged. * Classify captain holds from structured fields alone. Bucket membership was decided by several independent expressions, and two of them matched hold reason or body prose. That produced a recurring class of defects: holds that fell through every bucket and vanished from the board, and live decisions silently suppressed because their wording happened to contain a marker word - a reason of "non-deferred release choice" matched DEFERRED and disappeared. Replace all of it with one total classifier over structured fields only: hold_kind, state, hold_until, unresolved_blocker_ids, and the machine-written hold-set timestamp. Every captain hold gets exactly one hold_bucket - blocked, dated, aged, or live - so no hold can fall through and none can match two. captain_actionable is exactly the live bucket, and the --all-decisions reveal is a property of the bucket rather than a second filter. No hold reason or body prose is matched anywhere in the projection, so wording can no longer hide, reveal, or reclassify a decision. A hold that is superseded or no longer required is closed through the hold lifecycle instead of lingering as an open hold flagged by a keyword. * no-mistakes(review): Preserve working captain holds across bucket surfaces * no-mistakes(review): Reject pre-classifier secondmate summary caches * no-mistakes(review): Preserve complete live hold summaries * no-mistakes(review): Clarify working hold decision bucket semantics * no-mistakes(review): Reveal bounded remote holds and preserve blocker notes * no-mistakes(review): Make blocker overflow explicit in hold summaries * no-mistakes(document): Correct captain-hold projection documentation * no-mistakes(ci): Updated the stock macOS Bash CI snapshot expectation from 17 to 18 tests. Verified the suite under Bash 3.2.57: all 18 tests pass. `git diff --check` also passes * no-mistakes(ci): Updated the stock macOS Bash CI expectation from 51 to 53 Bearings tests. Verified all 53 pass under Bash 3.2.57; git diff --check passes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix a Bearings board bug in the firstmate repo: captain holds were rendering wrongly on the board, showing roughly 28 live "Captain's Call" entries that were mostly old parked items rather than decisions actually waiting on the captain.
The board decided where each captain hold belonged using several independent filter expressions, two of which matched natural-language text in the hold reason or body. That produced a recurring class of defects. Holds fell through every expression and vanished from the board entirely - a hold whose deferral date had arrived, a hold blocked by unresolved work, a hold on an actively working task, and remote holds restored from a pre-change cache. Separately, live decisions were silently suppressed because their wording happened to contain a marker word: a hold reasoned "non-deferred release choice" matched DEFERRED and disappeared from Captain's Call, even though the wording means the opposite.
Replace all of that with one total classifier that assigns every captain hold exactly one bucket, using structured fields only - hold_kind, state, hold_until, unresolved_blocker_ids, and the machine-written hold-set timestamp. The buckets are blocked when any blocker is unresolved, else dated while hold_until is still in the future, else aged when an undated hold is at least FM_SNAPSHOT_UNDATED_HOLD_AGE_DAYS old (default 14, measured from the hold-set timestamp so a fresh hold on an old task does not age, with legacy unstamped holds falling back to the task since date), else live. Being exhaustive and mutually exclusive is the point: no hold can fall through them all and none can match two, which is what ends both the disappearing-hold and the misclassification defects.
captain_actionable is exactly the live bucket. A live hold is a default Captain's Call entry. Blocked, dated and aged holds leave the default Captain's Call, render as Charted Next gates stating why - the blocking work, the until date, or the floored age - and are counted in the omitted[] disclosure. --all-decisions reveals every captain hold and drops its gate, so a hold is shown once and never twice. Remote secondmate summaries cached under the pre-change schema are refreshed or reported unavailable rather than fed through the projection with missing fields.
No hold reason or body prose is matched anywhere in the projection. Wording must never hide, reveal, or reclassify a decision. A hold that is superseded or no longer required is closed through the hold lifecycle rather than lingering as an open hold flagged by a keyword, so the parked-style and SUPERSEDED keyword matching is deliberately deleted rather than reimplemented. Re-holding with --until remains the durable way to defer a captain call.
Accepted documented limits, deliberately not fixed: a remote or secondmate hold retains the producing home age and aging decision rather than being recomputed against the parent threshold, and a rare concurrent answer-close and re-hold race can leave a re-held task without its age basis.
Scope note: tests/fm-herdr-lab.test.sh is deliberately identical to main. Earlier stabilization reworked it while chasing a flake that only appears under heavy local load, and asserted that a SIGSTOP-blocked process is gone after a timed-out provision, which is false on Linux because a stopped process does not die from SIGTERM. Keep main's version and do not reintroduce that process-liveness assertion.
Do not merge the pull request.
What Changed
--all-decisionsreveals all available holds without duplicate gates.Risk Assessment
✅ Low: The structured classifier, lifecycle timestamping, remote-schema boundary, projections, and behavioral regressions are internally consistent with the stated intent and prior authorized decisions.
Testing
The successful changed-test baseline was supplemented with focused Bearings, fleet-classifier, and hold-lifecycle tests plus an end-to-end CLI board fixture; structured buckets, prose-independent live classification, hold-set aging, omitted disclosure, and
--all-decisionsreveal behavior all worked as intended, with a reviewer-visible transcript captured and temporary worktree data removed.Evidence: Default and expanded Bearings boards demonstrating structured captain-hold buckets
Source: Default and expanded Bearings boards demonstrating structured captain-hold buckets
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 5 issues found → auto-fixed (6) ✅
bin/fm-bearings-snapshot.sh:487- This contradicts the required invariant that blocked, dated, and aged holds “render as Charted Next gates.” A captain-held in-flight task whose child still reportsworkingand whose bucket isblocked,dated, oragedis excluded fromdecisions_open, then the preceding state filter excludes it fromgates; it therefore still vanishes from the default board. Bucketed captain holds must bypass the working-state gate filter, while--all-decisionsshould continue removing their gate.bin/fm-fleet-snapshot.sh:945- This contradicts the requirement that a live hold is a default Captain's Call entry and specifically leaves the actively-working-task failure reachable for secondmates.$queued_allexcludes an in-flight held row when its child state isworking; consequently even ahold_bucket == "live"row never reachesdecisions_openorqueued, and the parent can show it only as Underway—even under--all-decisions. Preserve every bucketed captain hold in the secondmate decision/queued surfaces independently of active-child projection.bin/fm-fleet-snapshot.sh:1063- The intent requires “--all-decisions reveals every captain hold,” but secondmate summaries permanently slicequeuedanddecisions_open. For example, the 21st deferred captain hold with the default queued bound is absent from both exported arrays, so the parent cannot render its gate or reveal it with--all-decisions. The newly added documentation explicitly acknowledges this additional limitation, but the authoritative intent accepts only the producer-age and answer/re-hold race limits. Meeting the invariant requires an authorized decision about widening or bypassing the cross-home bounds..agents/skills/bearings/SKILL.md:45- The changed operational skill still instructs agents that parked-style reasons and explicit deferred/superseded/not-required prose control placement and that--all-decisionsreveals presentation hints. This directly contradicts “No hold reason or body prose is matched anywhere in the projection” and the new structured-only buckets. Remove the prose-classifier instructions and describe blocked/dated/aged/live bucket behavior instead; the script header contains the same stale claims.bin/fm-fleet-snapshot.sh:376- Simplification:current_prose_lines,resolution_leader, andhold_stamp_linewere introduced to prepare body prose for the now-deleted keyword classifier, but remain as a separate filtering path that changes the--fields bodiesexcerpt. No current intent requirement needs a special “current prose” representation once classification is structured-only. Remove these helpers and restore the direct bounded body excerpt unless this extra user-visible filtering is intentionally retained.🔧 Fix: Preserve working captain holds across bucket surfaces
1 error still open:
bin/fm-fleet-snapshot.sh:1315- Intent requires pre-change remote summaries to be “refreshed or reported unavailable,” but the v1 compatibility path validates new fields only on captain-hold rows. A pre-change cache produced while the original bug omitted a captain hold from both decisions_open and queued has no row to validate, so both all(...) checks pass and the stale v1 summary is accepted, preserving the disappearing hold. Remove the unrequired v1 compatibility path and enforce a summary-level schema/version boundary for the new classifier.🔧 Fix: Reject pre-classifier secondmate summary caches
1 warning still open:
bin/fm-bearings-snapshot.sh:348- Simplification:hold_summaryapplies the new 46/42 title/reason split to live holds even though only deferred holds need space reserved for an annotation. A live hold with a 60-character title and short reason previously retained its complete sub-90-character summary, but now silently loses title text. This contradicts the prior requirement that the default board remain unchanged. Preserve the original whole-summary truncation whenhold_noteis null, and reserve space only for blocked, dated, or aged annotations.🔧 Fix: Preserve complete live hold summaries
1 error still open:
bin/fm-bearings-snapshot.sh:442- The required invariant says each hold is “shown once and never twice,” but a captain-held in-flight task whose child reportsworkingis still added toin_flight_allat lines 423-432, then its bucket adds the same task to Captain's Call here when live or to Charted Next at lines 472-479 when blocked, dated, or aged. The secondmate path similarly exports it through bothactive_childrenand the hold surfaces. Thus the intended working-hold sequence now renders the same task in Underway and its hold bucket. Exclude bucketed captain holds from the corresponding main and secondmate Underway projections so each hold appears only in its assigned bucket.🔧 Fix: Clarify working hold decision bucket semantics
2 errors still open:
bin/fm-bearings-snapshot.sh:455- The requirement says--all-decisionsreveals every captain hold available within the remote-summary bound, but the queued-summary path accepts only deferred buckets. If a producing home exports one live hold indecisions_openand two inqueued(for example,FM_SNAPSHOT_SECONDMATE_DECISIONS=1and the default queued bound), the second live hold is present in the bounded summary yet is neither revealed nor gated under--all-decisions. Include all queued captain holds during expansion and deduplicate those already supplied bydecisions_open.bin/fm-bearings-snapshot.sh:351- The requiredblocked-by <work>explanation does not always survive the 90-character summary bound: the entire note plus reason receives only 42 characters. A short title with one 40-character blocker ID would fit within 90 characters, but this branch truncates the blocker identifier, and--all-decisionssimultaneously removes the gate containing the completeblocked_byfield. Reserve space for the complete note before allocating remaining space to title/reason.🔧 Fix: Reveal bounded remote holds and preserve blocker notes
1 error still open:
bin/fm-bearings-snapshot.sh:358---all-decisionsremoves the Charted Next gate, butfit(90)truncates the only remaining blocker note when its complete value exceeds 90 characters. A supported hold with two unresolved 40-character blocker IDs produces a note longer than 90 characters and silently loses part of the second ID. This conflicts with the authorized requirements that the complete blocked-by identity survive and that output remain within 90 characters; those constraints cannot both hold for multiple or longer blockers, so the desired output contract needs user resolution.🔧 Fix: Make blocker overflow explicit in hold summaries
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh --changed --exclude-family real-herdr-gatedBaseline:bin/fm-test-run.sh --changed --exclude-family real-herdr-gatedbin/fm-test-run.sh tests/fm-bearings-snapshot.test.shbin/fm-test-run.sh tests/fm-captain-hold-lifecycle.test.shbin/fm-test-run.sh tests/fm-fleet-snapshot-view.test.shManual end-to-end fixture throughbin/fm-bearings-snapshot.shin default and--all-decisionsmodes, with semanticjq -echecks of live, blocked, dated, aged, freshly re-held, disclosure, reveal, and gate-removal behaviorgit diff --quiet a5c64a0b6791b7643024436af9f737059d15cef2 -- tests/fm-herdr-lab.test.sh✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.