Skip to content

fix(bin): re-check open to-do items every pass, rank by oldest ask, widen coverage - #272

Closed
Amplify-Logic wants to merge 6 commits into
mainfrom
fm/todo-page-recheck-fixes
Closed

Amplify-Logic wants to merge 6 commits into
mainfrom
fm/todo-page-recheck-fixes

Conversation

@Amplify-Logic

Copy link
Copy Markdown
Owner

Intent

Build the to-do page fixes from the critical review of 8 Oct 2026 (full review at data/todo-page-recheck-fixes/review-report.md in the Firstmate home; read sections 3, 4 and 5). Lars approved the build: the page is "not working super well yet". The review found:

  • the 30-minute pass only takes in new messages and never re-reads open items, so items close only when Lars tells Firstmate (8 of 11 closures on 8 Oct), and 17 of 18 "Needs you now" rows were "not re-checked";
  • ranking puts the YellowBeard ticket Lars said to stop pushing first, and Firstmate tooling approvals above customer and partner asks, because partner-first depends on a rarely set flag and rows inside a tier sort by last-check time;
  • Slack DMs without an @mention, the calendar, and stale Asana tasks assigned to Lars never reach the page between morning sweeps; the open-tickets refresh and HubSpot re-scan get skipped;
  • small page bugs: Lars's own closures counted as "handled without you", raw UTC ticket times, "systems" passed as the unit list on watch lines, a title-less item, rotting "today/yesterday/N DAYS" in stored titles, a "waiting" with no date when the wait is on Lars, device updates on the page.
    The approved scope is the review's fixes 1, 3, 4 and 5 (section 5).

Substance of those referenced review fixes (section 5):
Fix 1 - the 30-minute pass is an intake, not a re-check: make fm-channel-intake.sh claim print a recheck: line for every open or waiting to-do item that has a source reference (ticket id, Slack channel and ts, Gmail thread), the same way it prints thread: lines; make the pass call fm-todo.sh sweep-start, then verify or close for each listed item; have complete report how many listed items were not re-read; change the lars-sent-replies coverage from "Nothing is observed onto the to-do page" to "close the matching open item with the sent message as evidence"; add two closing rules to daily-todo-freshness: a reply posted in the parent channel after the mention counts as a reply, and an internal hand-over never closes a partner-facing ask while the ticket still says Waiting on us.
Fix 3 - ranking uses the wrong signals: in fm-todo-compose.py sort inside a tier by when the ask was made, oldest first, not by last check; set the partner flag from the source (a HubSpot ticket with a partner or customer contact, a Slack mention naming a system id or customer site, an Asana Partner RMA, or a colleague relaying a partner question); put items whose source is Firstmate tooling in their own fold below partner and customer asks; the YellowBeard records are handled with mine (tracked, not surfaced), which already exists as a page command.
Fix 4 - coverage holes between morning sweeps: add a slack-lars-dms source that reads the known DM ids directly each pass; add a calendar-lars source for invites still needing a response and prep asks for the next two working days; give the Asana source a daily full re-list of incomplete tasks assigned to Lars; make complete for a hubspot-tickets source refuse unless tickets ran in the same pass; make the re-scan part of that same pass; make the coverage fold name what is not read.
Fix 5 - small page bugs: count actor "Lars" as "by you"; print ticket times in CEST; stop passing "systems" as the unit list; drop the -1.0 broken-sensor value from the freeze count; refuse a title-less observe; strip "today", "yesterday" and "N DAYS" from stored titles and compute ages at render time; refuse a resolve --waiting whose reason has no date when the wait is on Lars; keep device updates off the page.
Fix 2 (held decisions) is not part of this build.

What Changed

  • Every 30-minute pass now re-reads open items. fm-channel-intake.sh claim prints a recheck: line for each open or waiting to-do item that has an external source. The list comes from the new fm-todo.sh recheck command. complete then reports how many of those items were not re-read, and names them. fm-todo.sh sweep-start --pass records a pass, which raises the freshness floor only for items that can be re-read at their source. The daily-todo-freshness skill gains three closing rules: a reply in the parent channel counts as a reply, an internal hand-over never closes a partner-facing ask that is still Waiting on us, and the sent-replies harvest closes items with the sent message as evidence. The channel-intake docs describe the new pass flow.
  • Ranking and coverage changes.
    • In fm-todo-compose.py, items within a tier now sort by when the ask was made (oldest first), not by last-check time.
    • The partner/customer flag is now set from the source: a ticket with a partner contact, observe --partner, a 15-digit system id, or an Asana RMA.
    • Firstmate tooling asks move to their own "Firstmate tooling approvals" fold below partner and customer asks.
    • The intake gives each source kind its own extra read: Slack DM ids are read directly, the calendar covers today plus the next two working days, and Asana gets a once-a-day relist: with --relisted.
    • complete on a hubspot-tickets source is refused unless this pass refreshed the open-tickets snapshot, and also refused without --rescanned when a re-scan is due.
    • The coverage fold lists the channel families that no source reads between morning sweeps.
  • Page bug fixes.
    • Closures by the captain's configured captain_names now count as "by you".
    • Ticket last-in/last-out times are converted to the page's time zone.
    • --units must name unit ids or be none, and a freezing snapshot that lists a -1.0 reading is refused.
    • A new item with no title is refused.
    • "today", "yesterday" and "N days" wording is stripped from stored titles; the page computes ages when it renders.
    • resolve --waiting is refused when the wait is on the captain and the reason has no date.
    • Device-update telemetry is kept off the page.

Risk Assessment

✅ Low: The latest fix round only rewords the wait-date refusal and header to list the forms names_a_date accepts, and adds an executable test that a bare 'Sat' is refused. Re-tracing the earlier rounds turned up no remaining defect: pass floor with rereadable derived on every save, anchored month and weekday regex, relative-time stripping, tooling fold, the hubspot complete gating, and the recheck list and report.

Testing

The three targeted test files (fm-todo, fm-todo-render, fm-channel-intake) all pass. The real CLIs were also run end to end in a throwaway FM_HOME on a pinned 8 Oct clock; the transcript, the rendered page HTML and a screenshot are in the evidence directory. The run exercised the pass re-check and its count of skipped items, Lars's own closes counting as his, partner-first oldest-ask ranking with the tooling fold and the mine command, the new DM, calendar and Asana reads, the HubSpot pass gating, and each page-bug guard, including attempts to break them. Everything driven passed. The lab home and the browser profile were removed afterwards, and the worktree is clean. Only the closing rules written into the skill text were not driven, because they need a live orchestrator reading real Slack, HubSpot and Gmail.

  • Live validation: ✅ go - 9 of 10 scenarios driven live against the product
Scenario Result Live Evidence
A 30-minute pass claim lists a recheck: line for each open item that has a source ref, and complete reports how many listed items were not re-read ✅ pass live live-drive-transcript.txt: claim shows 9 recheck: lines carrying source:ref and leaves out the closed invoice item and the Firstmate approval; after 2 verifies, complete prints '7 of 9 listed not re-r…
An item Lars closed himself (actor Lars) shows as 'fulfilled by you' and is not counted as 'handled without you' ✅ pass live rendered-todo-page-2026-10-08.html: 'fulfilled by you · 10:00 CEST'; no 'handled without you'
Needs you ranks partner and customer asks first (from the --partner mark, a system id, an Asana RMA), oldest ask first; the Firstmate tooling approval sits in its own fold below; YellowBeard marked 'm… ✅ pass live Screenshot: Natalia relay (7 Oct), Partner RMA, black screen at 869951034894703, then tidy wiki and Kasper (5 Oct); 'Firstmate tooling approvals (1)' fold sits below the list; 'Yours, tracked but not…
Coverage: the slack-dms, calendar and Asana sources each get their own read on claim (DMs read directly, a two-working-day calendar window, one full Asana re-list per day), and --relisted is refused f… ✅ pass live Transcript: 'dms: D_DMS read each DM ... directly', 'calendar: CAL from: 2026-10-08 to: 2026-10-12', 'relist: A_REQ last_relist: never', no relist on the second claim that day, '--relisted requires an…
Adversarial: HubSpot complete is refused without this pass's open-tickets table, refused without --rescanned while the re-scan is due, and accepted once both are done ✅ pass live Transcript: 'the open-tickets table was not refreshed' exit 2; 'the re-scan is due' exit 2; then 'H_TICKETS read complete'
Adversarial: observe refuses a title-less new item, a 'systems' unit list and a -1.0 broken-sensor reading, and accepts a real freezing reading ✅ pass live Transcript: '--title is required' / '--units must name the newest units by id' / '-1.0 reading, the broken-sensor value' all exit 2; the 0.5 C reading is accepted and shows on the Watching line
Adversarial: resolve --waiting is refused when the wait is on Lars with no date (including '3 decisions', 'decide 2', bare 'Sat'); hand-overs to others and dated waits are accepted ✅ pass live Transcript: 4 refusals exit 2, each naming the accepted date forms; 'Queco will send you the logs', 'Lars handed it to Sara', 'until 14 Oct' and 'Friday' are accepted
Stored titles lose 'yesterday' and 'WAITING 14 DAYS' while 'by tomorrow' and real durations stay; ages are computed when the page renders ✅ pass live Page: 'PARTNER WAITING - Kasper asked about the spare filters' with 'asked Mon 5 Oct, 3 days ago'; 'Send the quote by tomorrow' and 'Return within 14 days?' unchanged; 'Approve 30 days extension' unch…
Ticket times show in CEST, not raw UTC, and a device update (OTA) stays off the page ✅ pass live Page: '<td>08:24 CEST</td>' for 06:24:04Z; the raw UTC string and 'thermostat OTA' are absent
The orchestrator follows the new daily-todo-freshness closing rules (a reply in the parent channel counts; an internal hand-over never closes a partner ask while the ticket says Waiting on us) and clo… ⏸️ untested no These are instructions for the orchestrator, not code. Proving them needs a live Firstmate primary that reads real Slack, HubSpot and Gmail with the captain's production connectors, which this isolate…
Evidence: Live CLI drive transcript (all scenarios)

Source: Live CLI drive transcript (all scenarios)

################ S5 adversarial: observe refusals
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791442800.1 --digest x  [at Thu 08 Oct 09:00]
fm-channel-intake: --title is required for a new item
  -> exit 2
$ fm-channel-intake.sh observe --source FLEET --condition b14 --count 3 --units systems --digest u  [at Thu 08 Oct 09:00]
fm-channel-intake: --units must name the newest units by id, or be none: systems
  -> exit 2
$ fm-channel-intake.sh observe --source FLEET --condition freezing --count 2 --units 867280069323517 (-1.0 C), 867280069323962 (0.5 C) --digest f  [at Thu 08 Oct 09:00]
fm-channel-intake: --units lists a -1.0 reading, the broken-sensor value; leave such units out of the freezing count and list
  -> exit 2
$ fm-channel-intake.sh observe --source FLEET --condition freezing --count 1 --units 867280069323962 (0.5 C) --digest g  [at Thu 08 Oct 09:00]
new 0d9043719e9914eaa0ab225824d1da941e3435d5e0bf16ccf9804ea65822b6be
  -> exit 0

################ seed asks (some days old, with rotting relative times)
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791183600.5 --digest a --class obligation --title tidy the internal wiki  [at Mon 05 Oct 09:00]
new 832e4a44f37697b546b1ebe4adf697333b7d3b828c3a9c602496bab323223fc9
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791183600.6 --digest a --class obligation --title PARTNER WAITING 14 DAYS - Kasper asked yesterday about the spare filters  [at Mon 05 Oct 09:00]
new c9641f0133bcdd5ddac77a2ffd14c3a6c80c6e41b8b0854d37d8107ed8f43dfd
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791356400.1 --digest b --class obligation --partner --title Natalia relays a dealer question on cooling  [at Wed 07 Oct 09:00]
new 2751c226fc1c32c10e970ece183a28f30b1295639691d140ee6b0599625424a8
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791442800.2 --digest c --class obligation --title black screen at 869951034894703  [at Thu 08 Oct 09:00]
new 8c89d71bf97ccd3ca5ca25f3d4954256226db33d8e51d5722ff9c3dac29cf8a8
  -> exit 0
$ fm-channel-intake.sh observe --source A_RMA --ref rma-17 --digest d --class obligation --title Partner RMA: tower return costs  [at Thu 08 Oct 09:00]
new 5da83eb6a40c7d67b12ce572bb8f169ed5a9c3e5940100e76b5bf4be4c769811
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791442800.3 --digest e --class obligation --title Send the quote by tomorrow  [at Thu 08 Oct 09:00]
new 45c96e9e4c3bbef20c694afe12e5f48a12552143cf5b6e820a7e8b5a08504dc2
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791442800.4 --digest e --class obligation --title Approve 30 days extension  [at Thu 08 Oct 09:00]
new b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791442800.7 --digest e --class obligation --title Return within 14 days?  [at Thu 08 Oct 09:00]
new be8cdfb58464caf3dbc7efbeb1a887601a579a9bcdc323f69da3a6ecc18ab7bb
  -> exit 0
$ fm-channel-intake.sh observe --source FLEET --ref ota-9 --digest o --class obligation --title Raise thermostat OTA? 3 systems  [at Thu 08 Oct 09:00]
new ece9834d9f2e9293c40f626f36239ccd3799c173e1261f9d94b99ded6dff573e
  -> exit 0
$ fm-channel-intake.sh observe --source C_SUPPORT --ref 1791442800.8 --digest h --class urgent --title invoice question from finance  [at Thu 08 Oct 09:00]
new 795305048f1868f0adcfa36a1071b343729df5f918a6007a2a587cd9c3f1faa8
  -> exit 0
$ fm-todo.sh sweep-start  [at Thu 08 Oct 09:00]
TODO_ITEMS: verification sweep started at 2026-10-08 09:00 CEST
  -> exit 0
$ fm-todo-render.sh render  [at Thu 08 Oct 09:00]
TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.JgILp1/.lavish/today-2026-10-08.html rendered at 09:00 CEST
  -> exit 0

################ S2: Lars closes one himself (actor Lars)
$ fm-todo.sh close --item t-d316332c6a90 --evidence Lars answered in the thread at 09:55 --actor Lars  [at Thu 08 Oct 10:00]
TODO_ITEMS: close t-d316332c6a90 -> closed
  -> exit 0

################ S1: 30-minute pass claim lists recheck: lines
$ fm-channel-intake.sh claim --source C_SUPPORT  [at Thu 08 Oct 10:30]
now: 1791448200
revision_window_seconds: 86400
revision_window_from: 1791361800
thread_tracking_window_seconds: 86400
source: C_SUPPORT	kind: slack-channel	checkpoint: 	coverage: support channel
recheck: t-3413461be975	e3b0c44298	open	hubspot	hubspot:t-45		YellowBeard consumption report
recheck: t-3e91ad539e95	cda0a61e81	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.4		Approve 30 days extension
recheck: t-44e4b905cdda	1a41dd9cae	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.7		Return within 14 days?
recheck: t-4e5cc29eb243	02235dcabb	open	asana-projects (A_RMA)	A_RMA:rma-17		Partner RMA: tower return costs
recheck: t-7fa932cca759	d4584cf191	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.5		tidy the internal wiki
recheck: t-b11cc4beb0e5	5236414222	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.3		Send the quote by tomorrow
recheck: t-c41c810b4446	4555a7b5f6	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791356400.1		Natalia relays a dealer question on cooling
recheck: t-ccc57e9b2767	38ee1565f9	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.2		black screen at 869951034894703
recheck: t-df4c3c4511ca	6d8ae0a010	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.6		PARTNER WAITING - Kasper asked about the spare filters
  -> exit 0
$ fm-todo.sh sweep-start --pass  [at Thu 08 Oct 10:30]
TODO_ITEMS: intake pass started at 2026-10-08 10:30 CEST
  -> exit 0
$ fm-todo.sh verify --item t-ccc57e9b2767 --how read the Slack thread  [at Thu 08 Oct 11:00]
TODO_ITEMS: verify t-ccc57e9b2767 -> open
  -> exit 0
$ fm-todo.sh verify --item t-c41c810b4446 --how read the Slack thread  [at Thu 08 Oct 11:00]
TODO_ITEMS: verify t-c41c810b4446 -> open
  -> exit 0
$ fm-channel-intake.sh complete --source C_SUPPORT --checkpoint p1  [at Thu 08 Oct 11:00]
CHANNEL_INTAKE: C_SUPPORT read complete, checkpoint p1
TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.JgILp1/.lavish/today-2026-10-08.html rendered at 11:00 CEST
CHANNEL_INTAKE: 7 of 9 listed not re-read: t-3413461be975 t-3e91ad539e95 t-44e4b905cdda t-4e5cc29eb243 t-7fa932cca759 t-b11cc4beb0e5 t-df4c3c4511ca
  -> exit 0

################ S4: coverage sources get their own reads
$ fm-channel-intake.sh claim --source D_DMS  [at Thu 08 Oct 10:30]
now: 1791448200
revision_window_seconds: 86400
revision_window_from: 1791361800
thread_tracking_window_seconds: 86400
source: D_DMS	kind: slack-dms	checkpoint: 	coverage: DMs with Natalia and Queco
dms: D_DMS	read each DM and group DM the coverage sentence names directly by its conversation id with the channel-read tool, never through search; observe every message to the captain since the checkpoint, whether or not it carries an @mention
recheck: t-3413461be975	e3b0c44298	open	hubspot	hubspot:t-45		YellowBeard consumption report
recheck: t-3e91ad539e95	cda0a61e81	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.4		Approve 30 days extension
recheck: t-44e4b905cdda	1a41dd9cae	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.7		Return within 14 days?
recheck: t-4e5cc29eb243	02235dcabb	open	asana-projects (A_RMA)	A_RMA:rma-17		Partner RMA: tower return costs
recheck: t-7fa932cca759	d4584cf191	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.5		tidy the internal wiki
recheck: t-b11cc4beb0e5	5236414222	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.3		Send the quote by tomorrow
recheck: t-c41c810b4446	4555a7b5f6	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791356400.1		Natalia relays a dealer question on cooling
recheck: t-ccc57e9b2767	38ee1565f9	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.2		black screen at 869951034894703
recheck: t-df4c3c4511ca	6d8ae0a010	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.6		PARTNER WAITING - Kasper asked about the spare filters
  -> exit 0
$ fm-channel-intake.sh claim --source CAL  [at Thu 08 Oct 10:30]
now: 1791448200
revision_window_seconds: 86400
revision_window_from: 1791361800
thread_tracking_window_seconds: 86400
source: CAL	kind: calendar	checkpoint: 	coverage: Lars calendar
calendar: CAL	fr

... [3080 bytes truncated] ...

p1/.lavish/today-2026-10-08.html rendered at 10:30 CEST
CHANNEL_INTAKE: 7 of 9 listed not re-read: t-3413461be975 t-3e91ad539e95 t-44e4b905cdda t-4e5cc29eb243 t-7fa932cca759 t-b11cc4beb0e5 t-df4c3c4511ca
  -> exit 0
(second claim same day: expect no relist)
$ fm-channel-intake.sh claim --source A_RMA  [at Thu 08 Oct 11:00]
now: 1791450000
revision_window_seconds: 86400
revision_window_from: 1791363600
thread_tracking_window_seconds: 86400
source: A_RMA	kind: asana-projects	checkpoint: a1	coverage: Partner RMA board and tasks assigned to Lars
recheck: t-3413461be975	e3b0c44298	open	hubspot	hubspot:t-45		YellowBeard consumption report
recheck: t-3e91ad539e95	cda0a61e81	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.4		Approve 30 days extension
recheck: t-44e4b905cdda	1a41dd9cae	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.7		Return within 14 days?
recheck: t-4e5cc29eb243	02235dcabb	open	asana-projects (A_RMA)	A_RMA:rma-17		Partner RMA: tower return costs
recheck: t-7fa932cca759	d4584cf191	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.5		tidy the internal wiki
recheck: t-b11cc4beb0e5	5236414222	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.3		Send the quote by tomorrow
recheck: t-c41c810b4446	4555a7b5f6	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791356400.1		Natalia relays a dealer question on cooling
recheck: t-ccc57e9b2767	38ee1565f9	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.2		black screen at 869951034894703
recheck: t-df4c3c4511ca	6d8ae0a010	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.6		PARTNER WAITING - Kasper asked about the spare filters
  -> exit 0
$ fm-channel-intake.sh complete --source C_SUPPORT --checkpoint x --relisted  [at Thu 08 Oct 10:30]
fm-channel-intake: --relisted requires an asana source
  -> exit 2

################ S4b adversarial: HubSpot pass needs its tickets + rescan
$ fm-channel-intake.sh claim --source H_TICKETS  [at Thu 08 Oct 10:30]
now: 1791448200
revision_window_seconds: 86400
revision_window_from: 1791361800
thread_tracking_window_seconds: 86400
source: H_TICKETS	kind: hubspot-tickets	checkpoint: 	coverage: Lars HubSpot tickets
stages: H_TICKETS	every open stage plus "Waiting on contact", which HubSpot marks closed but where a partner can still be waiting on the captain
tickets: H_TICKETS	hand the complete set of the captain's tickets whose stage is not Closed to tickets before complete; complete refuses without it
rescan: H_TICKETS	last_rescan: never	scope: every ticket in those stages, any owner, whose emails or notes name the captain or in which a colleague promised the customer that the tech team is on it, re-read in full whatever its last-modified date; observe each with --timeline-file, then complete with --rescanned
recheck: t-3413461be975	e3b0c44298	open	hubspot	hubspot:t-45		YellowBeard consumption report
recheck: t-3e91ad539e95	cda0a61e81	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.4		Approve 30 days extension
recheck: t-44e4b905cdda	1a41dd9cae	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.7		Return within 14 days?
recheck: t-4e5cc29eb243	02235dcabb	open	asana-projects (A_RMA)	A_RMA:rma-17		Partner RMA: tower return costs
recheck: t-7fa932cca759	d4584cf191	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.5		tidy the internal wiki
recheck: t-b11cc4beb0e5	5236414222	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.3		Send the quote by tomorrow
recheck: t-c41c810b4446	4555a7b5f6	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791356400.1		Natalia relays a dealer question on cooling
recheck: t-ccc57e9b2767	38ee1565f9	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791442800.2		black screen at 869951034894703
recheck: t-df4c3c4511ca	6d8ae0a010	open	slack-channel (C_SUPPORT)	C_SUPPORT:1791183600.6		PARTNER WAITING - Kasper asked about the spare filters
  -> exit 0
$ fm-channel-intake.sh complete --source H_TICKETS --checkpoint h1 --rescanned  [at Thu 08 Oct 10:30]
fm-channel-intake: H_TICKETS: hand this pass's open tickets to '~/.no-mistakes/worktrees/9957e108f4d7/01M4DQAQ9JR80R41PSXC9GGAZP/bin/fm-channel-intake.sh tickets' before complete; the open-tickets table was not refreshed
  -> exit 2
$ fm-channel-intake.sh tickets --owner captain < tickets.json
tickets: wrote 1 open tickets for captain read at 1791448200 to /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.JgILp1/data/channel-intake/tickets.json
  -> exit 0
$ fm-channel-intake.sh complete --source H_TICKETS --checkpoint h1  [at Thu 08 Oct 10:30]
fm-channel-intake: H_TICKETS: the re-scan is due; run it in this pass and complete with --rescanned
  -> exit 2
$ fm-channel-intake.sh complete --source H_TICKETS --checkpoint h1 --rescanned  [at Thu 08 Oct 10:30]
CHANNEL_INTAKE: H_TICKETS read complete, checkpoint h1
TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.JgILp1/.lavish/today-2026-10-08.html rendered at 10:30 CEST
CHANNEL_INTAKE: 7 of 9 listed not re-read: t-3413461be975 t-3e91ad539e95 t-44e4b905cdda t-4e5cc29eb243 t-7fa932cca759 t-b11cc4beb0e5 t-df4c3c4511ca
  -> exit 0

################ S5b: resolve --waiting date rule
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason waiting on Lars for 3 decisions  [at Thu 08 Oct 11:00]
fm-channel-intake: a wait on the captain needs a date in --reason (YYYY-MM-DD, a full weekday name or mon/tue/thu/fri, tomorrow, next week, or a day number with a month); without one it is a park with no end: waiting on Lars for 3 decisions
  -> exit 2
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason Lars will answer Sat  [at Thu 08 Oct 11:00]
fm-channel-intake: a wait on the captain needs a date in --reason (YYYY-MM-DD, a full weekday name or mon/tue/thu/fri, tomorrow, next week, or a day number with a month); without one it is a park with no end: Lars will answer Sat
  -> exit 2
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason waiting on you  [at Thu 08 Oct 11:00]
fm-channel-intake: a wait on the captain needs a date in --reason (YYYY-MM-DD, a full weekday name or mon/tue/thu/fri, tomorrow, next week, or a day number with a month); without one it is a park with no end: waiting on you
  -> exit 2
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason Lars will decide 2 options  [at Thu 08 Oct 11:00]
fm-channel-intake: a wait on the captain needs a date in --reason (YYYY-MM-DD, a full weekday name or mon/tue/thu/fri, tomorrow, next week, or a day number with a month); without one it is a park with no end: Lars will decide 2 options
  -> exit 2
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason Queco will send you the logs  [at Thu 08 Oct 11:00]
waiting b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836
  -> exit 0
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason Lars handed it to Sara  [at Thu 08 Oct 11:00]
waiting b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836
  -> exit 0
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason waiting on Lars until 14 Oct  [at Thu 08 Oct 11:00]
waiting b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836
  -> exit 0
$ fm-channel-intake.sh resolve --item b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836 --waiting --reason Lars will answer Friday  [at Thu 08 Oct 11:00]
waiting b6aa8d692be0595e6cfdc020f123d5a22721cdbfd73dcb02e1be6776b4d41836
  -> exit 0

################ S3: mine on YellowBeard; final render
$ fm-todo.sh command --item t-3413461be975 mine  [at Thu 08 Oct 11:00]
TODO_CMD: mine t-3413461be975 [hubspot:t-45] YellowBeard consumption report
  -> exit 0
$ fm-todo-render.sh render  [at Thu 08 Oct 15:00]
TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.JgILp1/.lavish/today-2026-10-08.html rendered at 15:00 CEST
  -> exit 0
Evidence: Live drive script

Source: Live drive script

#!/usr/bin/env bash
# Live drive of the to-do page fixes against a disposable FM_HOME.
set -u
ROOT=$1; LAB=$2
I="$ROOT/bin/fm-channel-intake.sh"; T="$ROOT/bin/fm-todo.sh"; R="$ROOT/bin/fm-todo-render.sh"
T0900=1791442800; T1000=1791446400; T1030=1791448200; T1100=1791450000; T1500=1791464400
OCT5=1791183600; OCT7=1791356400
intake() { local n=$1; shift; echo "\$ fm-channel-intake.sh $*  [at $(TZ=Europe/Amsterdam date -r $n '+%a %d %b %H:%M')]"; FM_HOME="$LAB" FM_ROOT_OVERRIDE="$ROOT" FM_CHANNEL_INTAKE_NOW="$n" "$I" "$@" 2>&1; echo "  -> exit $?"; }
todo() { local n=$1; shift; echo "\$ fm-todo.sh $*  [at $(TZ=Europe/Amsterdam date -r $n '+%a %d %b %H:%M')]"; FM_HOME="$LAB" FM_ROOT_OVERRIDE="$ROOT" FM_TODO_NOW="$n" FM_TODO_BACKLOG_OVERRIDE="$LAB/backlog.md" "$T" "$@" 2>&1; echo "  -> exit $?"; }
render() { local n=$1; echo "\$ fm-todo-render.sh render  [at $(TZ=Europe/Amsterdam date -r $n '+%a %d %b %H:%M')]"; FM_HOME="$LAB" FM_ROOT_OVERRIDE="$ROOT" FM_TODO_RENDER_NOW="$n" FM_TODO_BACKLOG_OVERRIDE="$LAB/backlog.md" "$R" render 2>&1 | tail -2; echo "  -> exit $?"; }
id_of() { FM_HOME="$LAB" FM_ROOT_OVERRIDE="$ROOT" FM_TODO_NOW=$T1500 FM_TODO_BACKLOG_OVERRIDE="$LAB/backlog.md" "$T" list | awk -F '\t' -v t="$1" 'index($5,t){print $1; exit}'; }
q() { FM_HOME="$LAB" FM_ROOT_OVERRIDE="$ROOT" FM_CHANNEL_INTAKE_NOW="$1" "$I" "${@:2}" >/dev/null 2>&1; }

mkdir -p "$LAB/config" "$LAB/data/channel-intake" "$LAB/.lavish"
printf 'enabled = true\ntimezone = Europe/Amsterdam\ninterval_seconds = 900\ncaptain_names = Lars Tolhurst\n' >"$LAB/config/channel-intake"
printf 'C_SUPPORT\tslack-channel\tsupport channel\nD_DMS\tslack-dms\tDMs with Natalia and Queco\nCAL\tcalendar\tLars calendar\nA_RMA\tasana-projects\tPartner RMA board and tasks assigned to Lars\nH_TICKETS\thubspot-tickets\tLars HubSpot tickets\nFLEET\ttelemetry-fleet-alerts\tfleet telemetry\n' >"$LAB/data/channel-intake/sources.tsv"
: >"$LAB/backlog.md"

echo "################ S5 adversarial: observe refusals"
intake $T0900 observe --source C_SUPPORT --ref 1791442800.1 --digest x
intake $T0900 observe --source FLEET --condition b14 --count 3 --units systems --digest u
intake $T0900 observe --source FLEET --condition freezing --count 2 --units '867280069323517 (-1.0 C), 867280069323962 (0.5 C)' --digest f
intake $T0900 observe --source FLEET --condition freezing --count 1 --units '867280069323962 (0.5 C)' --digest g

echo; echo "################ seed asks (some days old, with rotting relative times)"
intake $OCT5 observe --source C_SUPPORT --ref 1791183600.5 --digest a --class obligation --title 'tidy the internal wiki'
intake $OCT5 observe --source C_SUPPORT --ref 1791183600.6 --digest a --class obligation --title 'PARTNER WAITING 14 DAYS - Kasper asked yesterday about the spare filters'
intake $OCT7 observe --source C_SUPPORT --ref 1791356400.1 --digest b --class obligation --partner --title 'Natalia relays a dealer question on cooling'
intake $T0900 observe --source C_SUPPORT --ref 1791442800.2 --digest c --class obligation --title 'black screen at 869951034894703'
intake $T0900 observe --source A_RMA --ref rma-17 --digest d --class obligation --title 'Partner RMA: tower return costs'
intake $T0900 observe --source C_SUPPORT --ref 1791442800.3 --digest e --class obligation --title 'Send the quote by tomorrow'
intake $T0900 observe --source C_SUPPORT --ref 1791442800.4 --digest e --class obligation --title 'Approve 30 days extension'
intake $T0900 observe --source C_SUPPORT --ref 1791442800.7 --digest e --class obligation --title 'Return within 14 days?'
intake $T0900 observe --source FLEET --ref ota-9 --digest o --class obligation --title 'Raise thermostat OTA? 3 systems'
intake $T0900 observe --source C_SUPPORT --ref 1791442800.8 --digest h --class urgent --title 'invoice question from finance'
printf '{"version":2,"date":"2026-10-08","actions":[{"key":"k-t","source":"firstmate","ref":"voice-pr","class":"obligation","kind":"approval","title":"merge the voice fix?","updated":%s},{"key":"k-yb","source":"hubspot","ref":"t-45","class":"obligation","kind":"reply","title":"YellowBeard consumption report","partner_awaiting":true,"awaiting_since":%s,"updated":%s}]}\n' $T0900 $OCT5 $T0900 >"$LAB/.lavish/today-2026-10-08.morning.json"
todo $T0900 sweep-start
render $T0900

echo; echo "################ S2: Lars closes one himself (actor Lars)"
todo $T1000 close --item "$(id_of 'invoice question')" --evidence 'Lars answered in the thread at 09:55' --actor Lars

echo; echo "################ S1: 30-minute pass claim lists recheck: lines"
intake $T1030 claim --source C_SUPPORT
todo $T1030 sweep-start --pass
todo $T1100 verify --item "$(id_of 'black screen')" --how 'read the Slack thread'
todo $T1100 verify --item "$(id_of 'Natalia relays')" --how 'read the Slack thread'
intake $T1100 complete --source C_SUPPORT --checkpoint p1

echo; echo "################ S4: coverage sources get their own reads"
intake $T1030 claim --source D_DMS
intake $T1030 claim --source CAL
intake $T1030 claim --source A_RMA
intake $T1030 complete --source A_RMA --checkpoint a1 --relisted
echo "(second claim same day: expect no relist)"; intake $T1100 claim --source A_RMA
intake $T1030 complete --source C_SUPPORT --checkpoint x --relisted

echo; echo "################ S4b adversarial: HubSpot pass needs its tickets + rescan"
intake $T1030 claim --source H_TICKETS
intake $T1030 complete --source H_TICKETS --checkpoint h1 --rescanned
printf '[{"id":"49149551973","subject":"Problem with the unit","stage":"Waiting on us","last_in":"2026-10-08T06:24:04Z","last_out":"","link":"https://app.example/49149551973"}]' >"$LAB/tickets.json"
echo "\$ fm-channel-intake.sh tickets --owner captain < tickets.json"; FM_HOME="$LAB" FM_ROOT_OVERRIDE="$ROOT" FM_CHANNEL_INTAKE_NOW=$T1030 "$I" tickets --owner captain <"$LAB/tickets.json"; echo "  -> exit $?"
intake $T1030 complete --source H_TICKETS --checkpoint h1
intake $T1030 complete --source H_TICKETS --checkpoint h1 --rescanned

echo; echo "################ S5b: resolve --waiting date rule"
K=$(basename "$(grep -l "Approve 30 days" "$LAB"/data/channel-intake/items/*)")
for r in 'waiting on Lars for 3 decisions' 'Lars will answer Sat' 'waiting on you' 'Lars will decide 2 options'; do intake $T1100 resolve --item "$K" --waiting --reason "$r"; done
for r in 'Queco will send you the logs' 'Lars handed it to Sara' 'waiting on Lars until 14 Oct' 'Lars will answer Friday'; do intake $T1100 resolve --item "$K" --waiting --reason "$r"; done

echo; echo "################ S3: mine on YellowBeard; final render"
todo $T1100 command --item "$(id_of 'YellowBeard')" mine
render $T1500
Evidence: fm-todo test log

Source: fm-todo test log

ok - a pass judges every stored record by its aliases, whenever it was written
ok - an age counts calendar days across a clock change
ok - a time-bound ask closes by auto-expiry after its end, and a reopen or a missing end time stays open
ok - a Z end time expires its item, and a line the captain marked is left for him to clear
ok - a park holds off expiry only until it lapses
ok - an expired meeting moved later reopens and waits for its new end
ok - an expired ask the next sweep re-lists with no end time reopens
ok - verification changes only on a source read or an explicit verify, never on a sync or render
ok - done survives repeated syncs and the next morning; a changed ask resurfaces once with its reason
ok - an edit after a ledger resolution reopens the item once and the old resolution does not re-close it
ok - a reopen by any of its slots hides the source label, and a manual reason is shown as written
ok - stale pages are refused, repeats are harmless, and a handoff stays requested until accepted
ok - missing or corrupt input closes nothing and a released hold closes only its own item
ok - one ask seen twice dedupes, a snooze returns unverified, and only a named fulfilled close counts
ok - reopen brings a handed-over item back to the captain and refuses one already open
ok - captain holds nobody re-checked stay in the store and off the page
ok - routine activity older than the sweep stays on the page in its own not re-checked fold
ok - a retired routine record closes its item as superseded and an unreadable ledger closes nothing
ok - the routine not re-checked fold is capped at ten rows and a count of the rest
ok - sync prunes a retired routine record after thirty days and keeps every ask it ever tracked
ok - a routine line the captain marked is never retired by the intake, nor pruned
ok - a reopen note is not a captain mark: a revived routine thread still retires and prunes
ok - a partner-facing ask awaiting the captain ranks right after live problems and deadlines, oldest first and undated last
ok - each row marks the system it came from, and an unknown source gets a neutral marker
ok - a row merged from a morning line and a later ledger record marks the source it presents
ok - a pass lists every open item to re-check, counts the ones it skipped, keeps the closed boundary and leaves held decisions to the morning sweep
ok - Needs you ranks partners from the source, oldest ask first, folds tooling below, and honours mine
ok - titles lose relative times, device updates stay off, ticket times are local and coverage names its gaps
Evidence: fm-todo-render test log

Source: fm-todo-render test log

ok - the live section ranks a live outage first, then the oldest ask first, never the newest read
ok - waiting, closed-today and closed-earlier items each render in exactly one place
ok - each live line carries the time that item was read, separately from the render stamp, with its channel kept for audit
ok - legacy morning content stays in a labelled historical disclosure
ok - morning actions merge, resolutions win, and invalid metadata preserves the page
ok - fleet snapshots group by condition with newest counts and units
ok - the page is rebuilt from the records every time, so nothing is invented or carried forward
ok - the page renders from the tracked house-style templates with no visual tool installed
ok - a completed read refreshes an existing page, never manufactures one, and never fails on the render
ok - open tickets render live from the snapshot with its own read time and stored stage labels
ok - the snapshot writer refuses malformed input and keeps the previous snapshot
ok - an out-of-date snapshot says so with its read time
ok - a missing or corrupt snapshot says it could not be read
ok - a morning copy of the tickets table is replaced by the live section
ok - the out-of-date window follows the configured poll interval
ok - a snapshot whose consumed fields are not strings is reported, never crashed on
ok - dropping the morning copy leaves no empty panel or wrapper behind
ok - a ticket with no subject is written and rendered as an untitled ticket
ok - a legacy morning file holding only the tickets copy leaves no empty reference fold
ok - the calm layout ranks live problems and deadlines first in one list, never repeats an item, and folds the rest
Evidence: fm-channel-intake test log

Source: fm-channel-intake test log

ok - a home without an explicit opt-in is completely inert, so no clone or device self-enrolls
ok - a new ask appears once and an unchanged poll produces neither a second item nor a repeat ping
ok - an edit updates the same item, re-notifies once, and is reported as a correction
ok - a captain response clears the item with its evidence preserved, and no re-read reopens it
ok - an interrupted read retries from its own checkpoint, and an unavailable source stays unknown
ok - a gap while the laptop slept is caught up from each checkpoint, and freshness is exposed
ok - the interval has a floor, one armed cycle wakes once, a failing source backs off to a ceiling, and a quiet cycle succeeds
ok - a cross-source duplicate retains every provenance and stays one item
ok - notifications are refused until the recipient is verified, then grouped, rate-limited and severity-scoped
ok - quiet hours defer ordinary alerts without dropping them, and a service outage still gets through
ok - an automation candidate is a proposal only: nothing reaches the Action Deck and nothing becomes executable
ok - the brief and to-do list render from one ledger, so corrections and completions reconcile across both
ok - thread parents are tracked for re-read inside a bounded set, and both detection limits are disclosed on the brief
ok - the polled set stays bounded by moving aged routine traffic aside, without deleting evidence or forgetting an obligation
ok - ledger writes are serialized on a private mutex, so a sweep never clobbers an observation
ok - the intake never takes a fleet lock, starts a watcher, or touches another home records
ok - source identities and message content stay out of the tracked repository and out of config
registered: state/channel-intake.check.sh
registered: state/channel-intake.check.sh
registered: state/channel-intake.check.sh
registered: state/channel-intake.check.sh
registered: state/channel-intake.check.sh
ok - arming the live check is all-or-nothing and idempotent, and losing it is reported with its repair
ok - the watcher check wakes the primary once per state and re-arms when the state recurs
ok - a held alert with nothing due wakes once per state, with a 12-hour backstop
ok - a blocked or held alert is reported once rather than re-waking the primary every poll
ok - a cleared alert replaced by a different one still wakes the primary, and an unchanged one still does not
ok - a refused or capped notification is reported as itself instead of looking like a quiet home
ok - a requested render opens its page once, a background render never does, and a bad opener never fails the render
ok - a timezone that does not resolve is refused instead of silently becoming UTC
ok - quiet hours with equal bounds are refused instead of silencing every alert forever
ok - the scanning window bounds the reads themselves and opens with an immediate first read
ok - the schedule reuses the one launchd writer under its own agent label
ok - install and uninstall work against a temporary home and refuse a home that never opted in
ok - the session-start bootstrap section surfaces due sources, ready alerts and a lost live check
ok - a partner-facing ticket awaiting the captain is flagged by promise, note or unanswered message, only a reply discharges one, and a malformed or address-less timeline is refused
ok - awaiting partners lead the to-do, brief and alert order, and every rewrite keeps the facts
ok - a changed re-read with no timeline keeps an awaiting partner owed and on the to-do
ok - a timeline re-read of a resolved ticket records its current facts and leaves it resolved
ok - the re-scan cadence defaults around a long poll interval and refuses a configured value below it
ok - HubSpot claims cover Waiting on contact and a bounded periodic re-scan of every owner's tickets
ok - a HubSpot pass completes only with this pass's open-tickets table and the re-scan its claim handed out
ok - Slack DMs, the calendar and a daily Asana re-list each get their own read on a claim
ok - observe refuses untitled items, unit-less lists and the broken-sensor reading; a wait on the captain needs a date, a hand-over to others does not
Evidence: Pass re-check excerpt
$ fm-channel-intake.sh claim --source C_SUPPORT
recheck: t-ccc57e9b2767 ... C_SUPPORT:1791442800.2 black screen at 869951034894703
...
$ fm-channel-intake.sh complete --source C_SUPPORT --checkpoint p1
CHANNEL_INTAKE: 7 of 9 listed not re-read: t-3413461be975 ...

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed (4) ✅
  • 🚨 bin/fm-channel-intake.sh:1407 - complete checks whether the re-scan is due using its own time (rescan_due &#34;$id&#34; &#34;$epoch&#34;, where epoch is the time complete runs). The claim checked the same thing at claim time (line 1358). last_rescan is stamped with the time the previous complete ran, which is always later than its claim. So the deadline falls between two claims. If a claim lands just before the deadline, it hands out no rescan: line; the complete a few minutes later is past the deadline and refuses with "the re-scan is due". Example with the default 6 h interval: claim 09:00, complete --rescanned 09:05, then the claim at 15:00 is 5h55 later, so no re-scan is handed out; complete at 15:06 is 6h01 later, so it is refused. When rescan_interval is clamped to interval_seconds (line 521), this happens on nearly every pass. The orchestrator then cannot complete a pass it ran exactly as claimed, and the checkpoint stays put. Fix: judge the requirement at the claim's time. Pass the source's last_attempt (the claimed value already read at line 1401) to rescan_due instead of $epoch, so complete only requires a re-scan the claim actually handed out.
  • 🚨 bin/fm-todo-compose.py:186 - FLOOR now rises to the latest sweep-start --pass, which runs every 30 minutes. But recheck (bin/fm-todo-items.py:579-590, NOT_A_SOURCE) never lists items whose only aliases are firstmate-backlog:, firstmate: or ledger:, so a pass can never make them current again. Held backlog decisions are the worst case. They appear in Needs you only while current() holds (fm-todo-compose.py:514, not (held(r) and not current(r))). Example: the morning sweep verifies a held decision at 08:00, the 08:30 pass runs sweep-start --pass, and FLOOR moves to 08:30. The decision is no longer current, so it silently drops out of "Needs you now" for the rest of the day, and nothing in the pass can bring it back. Firstmate-internal morning lines likewise flip to "not re-checked" after the first pass with no way to re-read them. That works against the intent's goal of fewer "not re-checked" rows. Fix: keep the held-decision check (and freshness for items with no re-readable source) tied to MORNING_FLOOR, or include those items in the recheck list. Do not let a pass raise the floor for items it cannot list.
  • ⚠️ bin/fm-todo-items.py:48 - RELATIVE strips more than the intent asks for ("strip 'today', 'yesterday' and 'N DAYS' from stored titles"). It also removes "tomorrow", "tonight" and "this morning/afternoon/evening", and it removes any "N day(s)" anywhere in a title or why, including durations that are the substance of the ask. Examples: 'Send the quote by tomorrow' becomes 'Send the quote by'; 'Approve 30 days extension' becomes 'Approve extension'; 'Return within 14 days?' becomes 'Return within?'. These are wrong titles with no error, and the slot keeps the original so nothing shows the loss. Narrower form that still meets the intent: strip only today/yesterday, and "N days" only in waiting/age phrasings ("WAITING N DAYS", "N days ago", "(N days)"). Remove the extra tokens unless they are wanted.
  • ⚠️ bin/fm-todo-compose.py:231 - The intent says to "put items whose source is Firstmate tooling in their own fold". tooling() uses only an opt-in tooling: true flag, and only the morning sidecar can set it (fm-todo-items.py:236-246). The intake ledger, firstmate:-labelled morning lines without the flag, and firstmate-backlog decisions never land in the fold. They rank among the ordinary asks by age. This is the same "rarely set flag" weakness the review found in partner-first. A source-derived rule would match the stated criterion, for example label firstmate or a firstmate: / firstmate-backlog: alias when the item is not partner-facing. Needs confirmation of which signal is intended.
  • ⚠️ bin/fm-channel-intake.sh:1715 - waits_on_captain treats any reason containing "you", "your", "later", "captain" or any captain_names word as a wait on the captain. Ordinary hand-over reasons are therefore refused without a date, for example 'Lars handed it to Sara', 'Queco will send you the logs' or 'routed to Naomi, she will update your ticket'. In all of these the wait is on someone else. The intent only asks to refuse a dated-less --waiting "when the wait is on Lars". A narrower match would anchor on wait phrasings such as "waiting on/for <you|name>", "<name|you> will answer/decide" or "later".
  • ℹ️ bin/fm-todo-compose.py:378 - age() divides the difference between two local midnights by 86400 with floor division. Across the spring DST change that difference is n*86400-3600, so an ask from 7 days earlier shows "6 days ago" (and "1 day" becomes no age at all). Use round((day_start(NOW) - day_start(at)) / 86400), or compare calendar dates.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ bin/fm-todo-items.py:48 - RELATIVE strips more than the intent asks for ("strip 'today', 'yesterday' and 'N DAYS' from stored titles"). It also removes "tomorrow", "tonight" and "this morning/afternoon/evening", and it removes any "N day(s)" anywhere in a title or why, including durations that are the substance of the ask. Examples: 'Send the quote by tomorrow' becomes 'Send the quote by'; 'Approve 30 days extension' becomes 'Approve extension'; 'Return within 14 days?' becomes 'Return within?'. These are wrong titles with no error, and the slot keeps the original so nothing shows the loss. Narrower form that still meets the intent: strip only today/yesterday, and "N days" only in waiting/age phrasings ("WAITING N DAYS", "N days ago", "(N days)"). Remove the extra tokens unless they are wanted.
  • ⚠️ bin/fm-todo.sh:69 - The fix round changed two behaviours but left the fm-todo.sh header stale, and that header is the command's documented contract. (1) Lines 69-71 still say "today", "yesterday", "tomorrow", "tonight", "this morning" and "N days" are stripped from every stored title and why. After the fix to RELATIVE (bin/fm-todo-items.py:49), only today/yesterday, 'WAITING N DAYS', 'N days ago' and '(N days)' are stripped; tomorrow, tonight, this morning and other durations are kept on purpose. (2) Lines 54-57 say sweep-start --pass "raises the same freshness floor". After the fix to current() (bin/fm-todo-compose.py:200), a pass raises the floor only for items with a re-readable source ref. Held decisions and Firstmate-internal lines still answer to the morning sweep. An agent following the header would expect 'Send the quote by tomorrow' to be rewritten, and would expect a pass to demote a held decision. Update both paragraphs to match the code. The render header (bin/fm-todo-render.sh:88-92) and docs/channel-intake.md were already updated.
  • ℹ️ bin/fm-todo-items.py:343 - rereadable is set only in present(), and present() runs only for records that an observation touches in the current sync (fold, line 370). recheck_listed (line 588) instead computes source_refs live from the aliases. Some open records were stored before this change and are no longer re-observed, for example an item from an earlier day's morning sidecar that is absent from today's sidecar and from the ledger. Such a record has no rereadable field. current() then judges it against MORNING_FLOOR, even though recheck lists it for every pass. Example: such an item is verified at 08:00, a 10:30 pass lists it, and the orchestrator skips it. The page still shows "read 08:00" instead of "not re-checked", and only recheck --since counts the miss. This is transitional and the bug was introduced by this round's pass-floor fix. Fix: have sync call present() (or just set rereadable) on every loaded item, or derive the flag in one place from the aliases, so the composer and recheck use the same rule.

🔧 Fix applied.
1 warning still open:

  • ⚠️ bin/fm-channel-intake.sh:1725 - The names_a_date check added in round 1 (the waits-on-captain fix) matches month names only as unanchored prefixes, so ordinary words count as dates. [0-9]{1,2} ?(jan|…|dec) matches '3 decisions', and (jan|…|dec)[a-z]* [0-9]{1,2} matches 'decide 2', 'marked 2', 'market 3' and 'mayor 4'. Checked with the exact regex: 'waiting on Lars for 3 decisions', 'waiting on you, he marked 2 units' and 'Lars will decide 2 options' are all treated as dated. So resolve --waiting accepts these dateless parks on the captain, which is the case the intent says must be refused ('refuse a resolve --waiting whose reason has no date when the wait is on Lars'). There is no error; the item just becomes a wait with no end. The bare weekday abbreviations ('sat', 'sun', 'mon', 'wed') have the same weakness, e.g. 'he sat on it'. Fix: match whole month names or their standard abbreviations with word boundaries on both sides, e.g. (jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|june?|july?|aug(ust)?|sep(t(ember)?)?|oct(ober)?|nov(ember)?|dec(ember)?)[^a-z], require a boundary after the weekday token, and add a test that '… for 3 decisions' is still refused.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-channel-intake.sh:1725 - The names_a_date check added in round 1 (the waits-on-captain fix) matches month names only as unanchored prefixes, so ordinary words count as dates. [0-9]{1,2} ?(jan|…|dec) matches '3 decisions', and (jan|…|dec)[a-z]* [0-9]{1,2} matches 'decide 2', 'marked 2', 'market 3' and 'mayor 4'. Checked with the exact regex: 'waiting on Lars for 3 decisions', 'waiting on you, he marked 2 units' and 'Lars will decide 2 options' are all treated as dated. So resolve --waiting accepts these dateless parks on the captain, which is the case the intent says must be refused ('refuse a resolve --waiting whose reason has no date when the wait is on Lars'). There is no error; the item just becomes a wait with no end. The bare weekday abbreviations ('sat', 'sun', 'mon', 'wed') have the same weakness, e.g. 'he sat on it'. Fix: match whole month names or their standard abbreviations with word boundaries on both sides, e.g. (jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|june?|july?|aug(ust)?|sep(t(ember)?)?|oct(ober)?|nov(ember)?|dec(ember)?)[^a-z], require a boundary after the weekday token, and add a test that '… for 3 decisions' is still refused.
  • ℹ️ bin/fm-channel-intake.sh:1751 - Round 3 (the fix to names_a_date) dropped the bare abbreviations 'wed', 'sat' and 'sun' on purpose, because they are ordinary words ('he sat on it'). This partly departs from the round-3 instruction to accept the 'standard 3-letter abbreviation', which was given as an example for months. The trade-off itself is reasonable, and the check still fails closed. But the refusal message at line 1751 still says the reason may name 'a weekday'. So an orchestrator writing 'waiting on you, back Wed' or 'Lars will answer Sat' (checked: neither counts as dated) is refused with a message that says weekdays are accepted, and nothing tells it that these days need their full names. Fix: say in the message that Wednesday, Saturday and Sunday must be written in full. The header text at bin/fm-channel-intake.sh:217-221 also says only 'a weekday' and needs the same note.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 9 of 10 scenarios driven live against the product
Scenario Result Live Evidence
A 30-minute pass claim lists a recheck: line for each open item that has a source ref, and complete reports how many listed items were not re-read ✅ pass live live-drive-transcript.txt: claim shows 9 recheck: lines carrying source:ref and leaves out the closed invoice item and the Firstmate approval; after 2 verifies, complete prints '7 of 9 listed not re-r…
An item Lars closed himself (actor Lars) shows as 'fulfilled by you' and is not counted as 'handled without you' ✅ pass live rendered-todo-page-2026-10-08.html: 'fulfilled by you · 10:00 CEST'; no 'handled without you'
Needs you ranks partner and customer asks first (from the --partner mark, a system id, an Asana RMA), oldest ask first; the Firstmate tooling approval sits in its own fold below; YellowBeard marked 'm… ✅ pass live Screenshot: Natalia relay (7 Oct), Partner RMA, black screen at 869951034894703, then tidy wiki and Kasper (5 Oct); 'Firstmate tooling approvals (1)' fold sits below the list; 'Yours, tracked but not…
Coverage: the slack-dms, calendar and Asana sources each get their own read on claim (DMs read directly, a two-working-day calendar window, one full Asana re-list per day), and --relisted is refused f… ✅ pass live Transcript: 'dms: D_DMS read each DM ... directly', 'calendar: CAL from: 2026-10-08 to: 2026-10-12', 'relist: A_REQ last_relist: never', no relist on the second claim that day, '--relisted requires an…
Adversarial: HubSpot complete is refused without this pass's open-tickets table, refused without --rescanned while the re-scan is due, and accepted once both are done ✅ pass live Transcript: 'the open-tickets table was not refreshed' exit 2; 'the re-scan is due' exit 2; then 'H_TICKETS read complete'
Adversarial: observe refuses a title-less new item, a 'systems' unit list and a -1.0 broken-sensor reading, and accepts a real freezing reading ✅ pass live Transcript: '--title is required' / '--units must name the newest units by id' / '-1.0 reading, the broken-sensor value' all exit 2; the 0.5 C reading is accepted and shows on the Watching line
Adversarial: resolve --waiting is refused when the wait is on Lars with no date (including '3 decisions', 'decide 2', bare 'Sat'); hand-overs to others and dated waits are accepted ✅ pass live Transcript: 4 refusals exit 2, each naming the accepted date forms; 'Queco will send you the logs', 'Lars handed it to Sara', 'until 14 Oct' and 'Friday' are accepted
Stored titles lose 'yesterday' and 'WAITING 14 DAYS' while 'by tomorrow' and real durations stay; ages are computed when the page renders ✅ pass live Page: 'PARTNER WAITING - Kasper asked about the spare filters' with 'asked Mon 5 Oct, 3 days ago'; 'Send the quote by tomorrow' and 'Return within 14 days?' unchanged; 'Approve 30 days extension' unch…
Ticket times show in CEST, not raw UTC, and a device update (OTA) stays off the page ✅ pass live Page: '<td>08:24 CEST</td>' for 06:24:04Z; the raw UTC string and 'thermostat OTA' are absent
The orchestrator follows the new daily-todo-freshness closing rules (a reply in the parent channel counts; an internal hand-over never closes a partner ask while the ticket says Waiting on us) and clo… ⏸️ untested no These are instructions for the orchestrator, not code. Proving them needs a live Firstmate primary that reads real Slack, HubSpot and Gmail with the captain's production connectors, which this isolate…
  • bash tests/fm-todo.test.sh (28 ok, includes new recheck/ranking/page-bug/legacy-floor/DST-age tests)
  • bash tests/fm-todo-render.test.sh (all ok, oldest-ask ordering)
  • bash tests/fm-channel-intake.test.sh (all ok, HubSpot pass gating, new source kinds, observe/resolve refusals)
  • Live CLI drive live-drive.sh in a disposable FM_HOME (mktemp fm-lab.*): observe refusals, seeded asks, sweep-start, close --actor Lars, claim (recheck: lines), sweep-start --pass, verify, complete (skipped count), DM/calendar/Asana claims, --relisted guard, HubSpot complete refusals, resolve --waiting date rule, command mine, render
  • Headless Chrome screenshot of the rendered today-2026-10-08.html page
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
1 warning still open:

  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

… ask, widen coverage, fix page bugs

The 30-minute pass now lists every open to-do item with a source to
re-read and reports the ones it skipped; ranking derives the partner
tier from the source and orders by when the ask was made, with Firstmate
tooling approvals in their own fold; Slack DM, calendar and Asana
re-list reads join the claim and a HubSpot pass must refresh its tickets
and due re-scan; small page bugs (own closures, UTC ticket times, unit
lists, title-less items, rotting relative times, undated waits, device
updates) are fixed.
@AquabluData

Copy link
Copy Markdown

Superseded by #273, which carries these changes rebased onto main after #271 plus the later review fixes. Branch kept.

1 similar comment
@Amplify-Logic

Copy link
Copy Markdown
Owner Author

Superseded by #273, which carries these changes rebased onto main after #271 plus the later review fixes. Branch kept.

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.

2 participants