Skip to content

feat(bin): record team channel announcements as updates on the day page - #271

Merged
Amplify-Logic merged 5 commits into
mainfrom
fm/slack-announcements-intake
Oct 8, 2026
Merged

Amplify-Logic merged 5 commits into
mainfrom
fm/slack-announcements-intake

Conversation

@Amplify-Logic

Copy link
Copy Markdown
Owner

Intent

Poll the team channels for relevant updates, dates and features, and build it into the 30-minute pipeline. Lars's words: "I feel that we should also be polling these channels for relevant updates, dates, yeah, features. I think this AquaBoo furniture, the series two furniture is a good good example. So can we, like, build this into our pipeline?"

The example is Valerie Loutphi's post in #channel-team on 8 Oct 2026 14:21: "Aquablu Furniture (Limited Release)", a new cabinet for a complete REFILL+ Series 2 setup, a limited first batch of 50 units (pull-out FIZZ Pro drawer with spill capture, shelves for 6 flavour boxes, lockable doors, 1200 x 600 x 940 mm, 95 kg, no wheels; a wheeled version planned for end of year), with key dates (goes live on the webshop for partners to order), links to an internal fact sheet, Partner Wiki and partner email comms. Today such announcements are not read by the pass, which only reads @mentions of Lars, so launch dates, product changes and process updates reach Lars and the support agents only if he happens to see them.

What Changed

  • bin/fm-channel-intake.sh adds an update class, which only a slack-announcements source can use (observe rejects it from any other source). Updates never send a notification and never go on the to-do list. The brief lists them under what changed. On tick, an update moves to inactive/ after a 14-day UPDATE_WINDOW, counted from when it was posted or last edited, and it moves even if it was edited. Comments, --help text and log messages now name both the routine and update horizons.
  • bin/fm-todo-items.py, bin/fm-todo-compose.py and bin/fm-todo-render.sh map update ledger records to a new update kind that carries a posted time (the source epoch, or the creation time if there is none). The day page shows these in a collapsed "Updates (N)" fold, newest post first, with each post's time, label and link. A retired update is handled like retired routine chatter: it is not shown as a closure, and sync prunes it 30 days later.
  • docs/channel-intake.md adds a "Team announcement channels" section. It covers which posts to keep (launches and feature changes, dates and deadlines, price, availability or ordering changes, process or policy changes), the title and date convention, and when a post that asks for action gets an owed class instead. It also describes the durable data/product-updates/log.md entry the orchestrator appends, and adds an update row to the class table. The bootstrap-diagnostics and daily-todo-freshness skills point slack-announcements sources to that section, and new tests in tests/fm-channel-intake.test.sh and tests/fm-todo-render.test.sh cover the class gate, retirement and the Updates fold.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change adds a new update class that only a slack-announcements source can record, and I traced it through every path that handles classes: notify-due, the to-do list, the brief, retirement, the todo-items fold and prune, and the composer's Updates fold. It never becomes owed or a notification, it ages out on its own two-week horizon (corrected updates included), and existing routine and owed behaviour is unchanged.

Testing

The two targeted test files passed. I then drove the real fm-channel-intake.sh and fm-todo-render.sh against disposable lab homes. Steps covered: enrolling, tick, claim, the refused update from a non-announcement source, observing the Furniture post as an update, notify-due, todo, the brief, and the rendered day page (screenshot captured). I also re-observed an edit and advanced the clock past the 1-day and 14-day horizons, then rendered again. Every drivable scenario passed. The model-driven read of real team channels and the updates-log write are untested here because they need a live primary with the Slack connector. Lab homes were removed and the worktree is clean.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An enrolled slack-announcements source becomes due on the 30-minute tick and claim hands it out with its kind ✅ pass live 01-intake-cli-transcript.txt: tick prints '2 source(s) due'; claim lists 'source: C_TEAM kind: slack-announcements'
Adversarial: a non-announcement (@mention) source trying to record --class update is refused ✅ pass live 01-intake-cli-transcript.txt: 'fm-channel-intake: --class update requires a slack-announcements source', exit=2
The Furniture announcement observed as an update is listed in the brief under 'What changed', never notified, and never on the to-do list ✅ pass live 01-intake-cli-transcript.txt: items shows class update; notify-due prints nothing; todo lists only the RMA ask; brief 'What changed' lists both updates
The day page shows a collapsed 'Updates' fold below Needs you now, newest post first, each with its post time and permalink ✅ pass live 04-day-page-updates-fold.png and 03-day-page.html: Updates (2), Furniture 'posted 14:21 CEST' with Open link above 'posted Wed 7 Oct 12:46 CEST'; the RMA ask stays in Needs you now
An edited announcement (moved webshop date) updates the same item and the same page row instead of adding a second one ✅ pass live 07-edited-post-on-page-transcript.txt: 'updated faa624…' (same key); Updates (1) row now reads '22 Oct (moved)'
Updates outlast the 1-day routine horizon, leave after 14 days, and an edited update leaves 14 days after its last change ✅ pass live 05-edit-and-retirement-transcript.txt: both open at +2d; price list inactive at +14d30m while edited Furniture stays open; both inactive at +15d; status items_inactive: 2
Retired updates drop off the day page and are not reported as closed work ✅ pass live 06-day-page-after-retirement.html has no Updates fold and no Closed fold; the data/todo journal shows superseded/source closures
The live orchestrator reads the real #channel-team on its 30-minute pass, keeps only launch/date/price/process posts per docs/channel-intake.md, and appends data/product-updates/log.md ⏸️ untested no Needs a live primary with the authenticated Slack connector reading real team channels, plus model judgement over real posts. The gate's lab primary cannot reach Slack, and sending real channel conten…
Evidence: Intake CLI transcript: enrol, claim, refused update from mention source, Furniture update observed, notify-due/todo empty of it, brief lists it

Source: Intake CLI transcript: enrol, claim, refused update from mention source, Furniture update observed, notify-due/todo empty of it, brief lists it

$ fm-channel-intake.sh sources
C_MENTIONS	slack-channel	unknown	checkpoint=	last_ok=	failures=	@mentions of Lars
C_TEAM	slack-announcements	unknown	checkpoint=	last_ok=	failures=	#channel-team and #channel-announcements, kept posts only

$ fm-channel-intake.sh tick
CHANNEL_INTAKE: 2 source(s) due for channel-intake

$ fm-channel-intake.sh claim
now: 1791464054
revision_window_seconds: 86400
revision_window_from: 1791377654
thread_tracking_window_seconds: 86400
source: C_MENTIONS	kind: slack-channel	checkpoint: 	coverage: @mentions of Lars
source: C_TEAM	kind: slack-announcements	checkpoint: 	coverage: #channel-team and #channel-announcements, kept posts only

# adversarial: a mention source may not record an update
$ fm-channel-intake.sh observe --source C_MENTIONS --ref 1791462060.000100 --digest furniture --class update --title "Aquablu Furniture"
fm-channel-intake: --class update requires a slack-announcements source
exit=2

# Valerie Loutphi, #channel-team, 8 Oct 2026 14:21 - kept as an update
$ fm-channel-intake.sh observe --source C_TEAM --ref 1791462060.000100 --class update --source-epoch 1791462060 ...
new faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2
exit=0

# an older announcement (price list) posted yesterday
new 23aed4d42fe90e0770c7f09e8a3e8f4520114128e3b5c92e016ed630c8a40ea3
exit=0

# an ask for Lars from the mention source
new 3226f8c82c0502b7cf93c26030804ebfc3c03645525deda07cf8d0a0e9744e1f
exit=0

$ fm-channel-intake.sh complete --source C_TEAM ...
CHANNEL_INTAKE: C_TEAM read complete, checkpoint 1791462060.000100
TODO_RENDER: no page at /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.Xjy9Rf/.lavish/today-2026-10-08.html yet; nothing refreshed
CHANNEL_INTAKE: C_MENTIONS read complete, checkpoint 1791463000.000300
TODO_RENDER: no page at /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.Xjy9Rf/.lavish/today-2026-10-08.html yet; nothing refreshed

$ fm-channel-intake.sh items
23aed4d42fe90e0770c7f09e8a3e8f4520114128e3b5c92e016ed630c8a40ea3	open	update	C_TEAM	Flavour box price list changes from 1 Nov
3226f8c82c0502b7cf93c26030804ebfc3c03645525deda07cf8d0a0e9744e1f	open	obligation	C_MENTIONS	Approve the RMA for Hotel Krasnapolsky
faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2	open	update	C_TEAM	Aquablu Furniture limited release (REFILL+ Series 2 cabinet), 50 units - live on the partner webshop 15 Oct

$ fm-channel-intake.sh notify-due
exit=0

$ fm-channel-intake.sh todo
# Daily to-do - 2026-10-08

Human obligations only. Executable automations live in the Action Deck behind their own approval.

- [ ] Approve the RMA for Hotel Krasnapolsky (C_MENTIONS)

## Waiting on others

- nothing

$ fm-channel-intake.sh brief
# Channel intake brief - 2026-10-08 14:54 CEST

## Coverage

Enrolled sources only. Enrolling these does not enrol every channel, mailbox or board in the workspace.
A source reading `unknown` did not complete its last read; that is not the same as nothing new.

- `C_MENTIONS` (slack-channel): fresh - last read 0s ago, @mentions of Lars
- `C_TEAM` (slack-announcements): fresh - last read 0s ago, #channel-team and #channel-announcements, kept posts only

## What changed

- Flavour box price list changes from 1 Nov (C_TEAM)
- Aquablu Furniture limited release (REFILL+ Series 2 cabinet), 50 units - live on the partner webshop 15 Oct (C_TEAM)

## What needs you

- [obligation] Approve the RMA for Hotel Krasnapolsky (C_MENTIONS, first seen 2026-10-08)

## Waiting on others

- nothing

## Next dated actions

- nothing

## Automation candidates

Proposals only. Nothing here is prepared, staged or executable, and detecting one grants no permission to act.

- nothing

## Detection limits

- An in-place edit older than the 86400s revision window is not detected.
- A reply on a thread whose parent is not in the tracked set can appear in no cursor read.
- The tracked set is bounded, not forever: a parent whose reply marker has not advanced within 86400s leaves it, and replies on a parent that has left are not re-read.
- Routine traffic older than 86400s leaves the polled set and is listed by `items --state inactive`. Nothing is deleted, and nothing owed, waiting or corrected ever leaves.
- The 1800s poll interval is a target detection latency, not a guaranteed upper bound: sleep, offline stretches, backoff and queue delay all add to it.
- Measured detection latency so far: 2 sample(s), mean 48024s, max 94054s
Evidence: Render command output

Source: Render command output

TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.Xjy9Rf/.lavish/today-2026-10-08.html rendered at 14:54 CEST
Evidence: Rendered day page (8 Oct) with Updates (2) fold

Source: Rendered day page (8 Oct) with Updates (2) fold

<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Today - Thursday 8 October 2026</title>
<style>
:root{
  --bg:#f4f7fb; --panel:#ffffff; --ink:#132033; --ink-2:#3e4f66; --ink-3:#6b7a90;
  --line:#d7e0ec; --line-2:#e8eef6; --accent:#0b4f8a; --accent-ink:#ffffff; --accent-soft:#e3eef9;
  --ok:#1d7a4f; --ok-soft:#e2f3ea; --warn:#9a6300; --warn-soft:#fdf1d8; --bad:#a8322b; --bad-soft:#fbe5e3; --info:#2f5f9e; --info-soft:#e6eef9;
  --obs:#0b4f8a; --inf:#7a5a00; --unv:#7a2e80;
  --serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif;
  --mono:"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
}
@media (prefers-color-scheme: dark){
  :root{
    --bg:#0e1622; --panel:#141f2e; --ink:#e6edf6; --ink-2:#b7c4d6; --ink-3:#8593a8;
    --line:#26354a; --line-2:#1d2a3c; --accent:#6fb1ea; --accent-ink:#0b1a2b; --accent-soft:#15304a;
    --ok:#5fcf92; --ok-soft:#123324; --warn:#f0c264; --warn-soft:#3a2d0c; --bad:#f08a82; --bad-soft:#3d1a18; --info:#8fb6ec; --info-soft:#17304d;
    --obs:#6fb1ea; --inf:#e0b95a; --unv:#cf8fd6;
  }
}
*{box-sizing:border-box;min-width:0}
html,body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.45}
a{color:var(--accent);text-decoration:none;border-bottom:1px solid transparent}
a:hover{border-bottom-color:var(--accent)}
.wrap{max-width:1180px;margin:0 auto;padding:28px 22px 60px}
header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:12px 24px;margin-bottom:18px}
h1{font-family:var(--serif);font-weight:600;font-size:34px;line-height:1.1;margin:0;letter-spacing:-.01em}
h2{font-family:var(--serif);font-weight:600;font-size:22px;margin:34px 0 10px;letter-spacing:-.005em}
h2 small{font-family:var(--sans);font-weight:500;font-size:13px;color:var(--ink-3);margin-left:10px}
h3.sub{font-family:var(--sans);font-size:13px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);margin:22px 0 8px}
.kicker{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);margin-bottom:6px}
.meta{font-size:13px;color:var(--ink-2);text-align:right;max-width:540px}
.meta .mono{font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--ink-2)}
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin:18px 0 6px}
.tile{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.tile .n{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:30px;font-weight:600;line-height:1;color:var(--accent)}
.tile .l{font-size:13px;color:var(--ink-2);margin-top:6px}
.tile .s{font-size:12px;color:var(--ink-3);margin-top:2px}
.note{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:8px;padding:12px 16px;font-size:14px;color:var(--ink-2);margin:14px 0}
.note b{color:var(--ink)}
.note.warnline{border-left-color:var(--warn)}
table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:14px}
.tablewrap{overflow-x:auto;border-radius:10px}
thead th{text-align:left;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);font-weight:600;padding:10px 12px;border-bottom:1px solid var(--line);background:var(--panel)}
tbody td{padding:12px 12px;border-bottom:1px solid var(--line-2);vertical-align:top}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover td{background:var(--accent-soft)}
tr.closed td{color:var(--ink-3)}
tr.replyrow{display:none}
tr.replyrow.open{display:table-row}
tr.replyrow td{padding:0 12px 12px;border-bottom:1px solid var(--line-2);background:var(--panel)}
tbody tr.replyrow:hover td{background:var(--panel)}
button.notetoggle{font-family:var(--mono);font-size:11px;letter-spacing:.04em;padding:3px 8px;margin-left:6px;border:1px dashed var(--line);border-radius:6px;background:transparent;color:var(--ink-3);cursor:pointer;vertical-align:top}
button.notetoggle:hover{color:var(--ink-2);border-color:var(--accent)}
button.notetoggle[aria-expanded=true]{border-style:solid;color:var(--ink-2)}
form.reply{display:flex;gap:8px;align-items:center}
form.reply input[type=text]{flex:1;min-width:0;font:inherit;font-size:13px;padding:6px 10px;border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--ink)}
form.reply input[type=text]::placeholder{color:var(--ink-3)}
form.reply button{font:inherit;font-size:12.5px;padding:6px 14px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink-2);cursor:pointer;white-space:nowrap}
form.reply button:hover{border-color:var(--accent);color:var(--ink)}
form.reply .ack{font-size:12px;color:var(--ok);white-space:nowrap}
tr.closed td.who{color:var(--ink-2)}
td.who{font-weight:600;white-space:nowrap}
td.who .src{display:flex;align-items:center;gap:4px;margin-top:5px;font-size:11.5px;font-weight:500;color:var(--ink-3);white-space:nowrap}
td.who .src svg{width:12px;height:12px;flex:none;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
td.who .org{display:block;font-weight:400;font-size:12.5px;color:var(--ink-3)}
td.what{min-width:320px;color:var(--ink)}
td.what .ctx{display:block;color:var(--ink-2);font-size:13px;margin-top:3px}
td.what .next{display:block;color:var(--ink);font-size:13px;margin-top:6px}
td.what .next b{color:var(--accent)}
td.since{white-space:nowrap;font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:13px;color:var(--ink-2)}
td.links{white-space:nowrap}
td.links a{display:inline-block;font-size:12.5px;padding:3px 8px;border:1px solid var(--line);border-radius:6px;margin:0 4px 4px 0;background:var(--bg)}
td.links a:hover{border-color:var(--accent)}
td.links .nolink{display:inline-block;font-size:12px;color:var(--ink-3);font-style:italic}
.pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:2px 8px;border-radius:999px;white-space:nowrap}
.pill.bad{background:var(--bad-soft);color:var(--bad)}
.pill.warn{background:var(--warn-soft);color:var(--warn)}
.pill.ok{background:var(--ok-soft);color:var(--ok)}
.pill.info{background:var(--info-soft);color:var(--info)}
.prov{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.04em;padding:1px 6px;border-radius:4px;border:1px solid currentColor;margin:6px 0 0 8px}
.prov.obs{color:var(--obs)} .prov.inf{color:var(--inf)} .prov.unv{color:var(--unv)}
.id{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:12.5px;color:var(--ink-3)}
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.card h3{font-family:var(--serif);font-size:17px;margin:0 0 8px;font-weight:600}
.card ol,.card ul{margin:0;padding-left:20px;color:var(--ink-2);font-size:14px}
.card li{margin:5px 0}
.card li b{color:var(--ink)}
.rec{margin-top:8px;font-size:13px;color:var(--ink-2)}
.rec b{color:var(--ok)}
.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12.5px;color:var(--ink-3);margin-top:28px;padding-top:14px;border-top:1px solid var(--line)}
.done{color:var(--ink-2);font-size:14px}
.done li{margin:3px 0}
@media print{ body{background:#fff} .tile,.card,table{break-inside:avoid} }

/* --- private improved variant: presentation only --- */
td.since .lab{display:block;font-family:var(--sans);font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3);font-weight:600}
td.since .age{display:block;font-family:var(--sans);font-size:12px;color:var(--ink-3);margin-top:2px;white-space:normal}
.strips{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:14px;margin:16px 0 6px}
@media (max-width:760px){.strips{grid-template-columns:minmax(0,1fr)}}
.strip{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.strip.now{border-left:4px solid var(--accent)}
.strip.unv{border-left:4px solid var(--unv)}
.strip h3{font-family:var(--serif);font-size:17px;font-weight:600;margin:0 0 2px}
.strip .sub{font-size:12.5px;color:var(--ink-3);margin:0 0 10px}
.strip .grp{font-size:11px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin:12px 0 6px}
.strip .grp:first-of-type{margin-top:0}
.strip ol{margin:0;padding-left:20px;font-size:14px;color:var(--ink-2)}
.strip ol li{margin:7px 0}
.strip ul{margin:0;padding-left:18px;font-size:13.5px;color:var(--ink-2)}
.strip ul li{margin:7px 0}
.strip a.jump{font-weight:600;border-bottom:1px solid transparent}
.strip a.jump:hover{border-bottom-color:var(--accent)}
.strip .why{display:block;font-size:12.5px;color:var(--ink-3);margin-top:2px}
tr:target td{background:var(--accent-soft)}
details{margin:20px 0;border-top:1px solid var(--line);padding-top:12px}
summary{cursor:pointer;font-weight:600;color:var(--ink-2)}
.watch-line{padding:12px 16px;margin:8px 0;background:var(--panel);border:1px solid var(--line);border-radius:8px}
.done .why{display:block;font-size:12.5px;color:var(--ink-3);margin-top:2px}
td.what .ctx.how{font-size:12px;color:var(--ink-3)}
.strip.now{margin:16px 0 6px}
@media (max-width:600px){
  .wrap{padding:18px 16px 48px}
  td.what{min-width:0}
  .meta{text-align:left}
  tr.item{display:grid;grid-template-columns:minmax(0,1fr)}
  tr.item td{border-bottom:none;padding:8px 12px}
  tr.item td:last-child{border-bottom:1px solid var(--line-2)}
  tr.item td.who,tr.item td.links{white-space:normal}
  td.who .src{display:inline-flex;margin:0 0 0 8px;vertical-align:middle}
}
.watch-line .why{display:block;color:var(--ink-3);font-size:13px;margin-top:5px;overflow-wrap:anywhere}
</style>
</head><body><div class="wrap">
<header><div><div class="kicker">Aquablu Starship</div><h1>Today</h1></div>
<div class="meta">Thursday 8 October 2026<br>Page rebuilt from the to-do records at <span class="mono">14:54 CEST</span>.<br>Each line carries its own last check.</div></header>
<h2>Needs you now<small>live problems and deadlines first, then partners waiting on you</small></h2>
<div class="tablewrap"><table><tbody>
<tr class="item" id="item-t-23789a45380e" data-source="slack-channel (C_MENTIONS)" data-how="read by the channel intake on slack-channel (C_MENTIONS)"><td class="who"><span class="pill info">obligation</span><span class="src" title="From Slack" aria-label="From Slack"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z"/></svg>Slack</span></td>
<td class="what">Approve the RMA for Hotel Krasnapolsky<span class="prov obs">read 14:54 CEST</span></td>
<td class="links"><span class="nolink">no link recorded</span><button type="button" class="notetoggle" aria-expanded="false" onclick="var r=document.getElementById(&#x27;note-t-23789a45380e&#x27;);var o=r.classList.toggle(&#x27;open&#x27;);this.setAttribute(&#x27;aria-expanded&#x27;,o);if(o){r.querySelector(&#x27;input&#x27;).focus();}">note</button></td></tr>
<tr class="replyrow" id="note-t-23789a45380e"><td colspan="3"><form class="reply" data-lavish-question="todo-item-t-23789a45380e" onsubmit="event.preventDefault();var f=event.currentTarget,v=f.note.value.trim();if(!v)return;if(!window.lavish||!window.lavish.queuePrompt){f.querySelector(&#x27;.ack&#x27;).textContent=&#x27;review session not connected&#x27;;return;}window.lavish.queuePrompt(&#x27;Update on to-do item &#x27;+&quot;t-23789a45380e&quot;+&#x27; rev &#x27;+&quot;4a8963ecbd&quot;+&#x27; (&#x27;+&quot;Approve the RMA for Hotel Krasnapolsky&quot;+&#x27;): &#x27;+v,{tag:&#x27;item-update&#x27;,text:&quot;Approve the RMA for Hotel Krasnapolsky&quot;+&#x27;: &#x27;+v,element:f,queueKey:&quot;todo-item-t-23789a45380e&quot;,data:{item_id:&quot;t-23789a45380e&quot;,rev:&quot;4a8963ecbd&quot;,title:&quot;Approve the RMA for Hotel Krasnapolsky&quot;,note:v}});f.note.value=&#x27;&#x27;;f.querySelector(&#x27;.ack&#x27;).textContent=&#x27;queued&#x27;;"><input type="text" name="note" autocomplete="off" placeholder="drop, done, park til Friday, mine, you: what to do, dig, or anything else"><button type="submit">Queue</button><span class="ack"></span></form></td></tr>
</tbody></table></div>
\<details><summary>Updates (2)</summary>
<div class="tablewrap"><table><tbody>
<tr id="item-t-440a9e53003e" data-source="slack-announcements (C_TEAM)" data-how="read by the channel intake on slack-announcements (C_TEAM)"><td>Aquablu Furniture limited release (REFILL+ Series 2 cabinet), 50 units - live on the partner webshop 15 Oct</td><td><span class="prov inf">posted 14:21 CEST · slack-announcements (C_TEAM)</span></td><td><a href="https://aquablu.slack.com/archives/C_TEAM/p1791462060000100" target="_blank" rel="noreferrer">Open</a></td></tr>
<tr id="item-t-5032fb7f8e39" data-source="slack-announcements (C_TEAM)" data-how="read by the channel intake on slack-announcements (C_TEAM)"><td>Flavour box price list changes from 1 Nov</td><td><span class="prov inf">posted Wed 7 Oct 12:46 CEST · slack-announcements (C_TEAM)</span></td><td><span class="nolink">no link recorded</span></td></tr>
</tbody></table></div>
</details>
<h2>Your open tickets<small>not available</small></h2>
<p class="sub"><span class="prov unv">Could not read your open tickets: no HubSpot read has been recorded.</span></p>
\<details><summary>Intake coverage</summary>
<ul class="done"><li><b>C_MENTIONS</b> - last successful read 14:54 CEST</li><li><b>C_TEAM</b> - last successful read 14:54 CEST</li></ul>
</details>

<div class="legend">
<span><span class="prov obs">observed</span> read from the source system at the time stamped on the row</span>
<span><span class="prov inf">inferred</span> reasoned from evidence, not stated outright</span>
<span><span class="prov unv">unverifiable</span> could not be confirmed from any source available here</span>
</div>

</div></body></html>
![Screenshot: RMA ask in Needs you now; Updates fold with the Furniture post (posted 14:21 CEST, Open link) above the 7 Oct price-list update](https://github.com/user-attachments/assets/c237108e-e62d-43b0-80d9-16c41ae63cb5) - Evidence: [Screenshot: RMA ask in Needs you now; Updates fold with the Furniture post (posted 14:21 CEST, Open link) above the 7 Oct price-list update](https://github.com/Amplify-Logic/firstmate/blob/3e8ba624f25af6e1c22172da2ce5614b99bf6589/.no-mistakes/evidence/fm/slack-announcements-intake/04-day-page-updates-fold.png)
Evidence: Edit and retirement over time (+2d stays; +14d unedited retires; edited retires at +15d)

Source: Edit and retirement over time (+2d stays; +14d unedited retires; edited retires at +15d)

# T0 = Thu 08 Oct 14:54

# T0+1h: Valerie edits the post, moving the webshop date (re-observe same ref)
updated faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2

# T0+2d (Sat 10 Oct 14:54): tick - routine would have retired after 1 day; updates must stay
CHANNEL_INTAKE: 2 source(s) due for channel-intake
23aed4d42fe90e0770c7f09e8a3e8f4520114128e3b5c92e016ed630c8a40ea3	open	update	C_TEAM	Flavour box price list changes from 1 Nov
3226f8c82c0502b7cf93c26030804ebfc3c03645525deda07cf8d0a0e9744e1f	open	obligation	C_MENTIONS	Approve the RMA for Hotel Krasnapolsky
faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2	open	update	C_TEAM	Aquablu Furniture limited release (REFILL+ Series 2 cabinet), 50 units - live on the partner webshop 22 Oct (moved from 15 Oct)

# T0+14d+30m (Thu 22 Oct 15:24): tick - unedited price-list update past its 14-day horizon retires; edited Furniture (last change T0+1h) stays
CHANNEL_INTAKE: 2 source(s) due for channel-intake
-- items --state open
3226f8c82c0502b7cf93c26030804ebfc3c03645525deda07cf8d0a0e9744e1f	open	obligation	C_MENTIONS	Approve the RMA for Hotel Krasnapolsky
faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2	open	update	C_TEAM	Aquablu Furniture limited release (REFILL+ Series 2 cabinet), 50 units - live on the partner webshop 22 Oct (moved from 15 Oct)
-- items --state inactive
23aed4d42fe90e0770c7f09e8a3e8f4520114128e3b5c92e016ed630c8a40ea3	open	update	C_TEAM	Flavour box price list changes from 1 Nov

# T0+15d (Fri 23 Oct 14:54): tick - edited Furniture now past its horizon too
CHANNEL_INTAKE: 2 source(s) due for channel-intake
-- items --state open
3226f8c82c0502b7cf93c26030804ebfc3c03645525deda07cf8d0a0e9744e1f	open	obligation	C_MENTIONS	Approve the RMA for Hotel Krasnapolsky
-- items --state inactive
23aed4d42fe90e0770c7f09e8a3e8f4520114128e3b5c92e016ed630c8a40ea3	open	update	C_TEAM	Flavour box price list changes from 1 Nov
faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2	open	update	C_TEAM	Aquablu Furniture limited release (REFILL+ Series 2 cabinet), 50 units - live on the partner webshop 22 Oct (moved from 15 Oct)

-- status
items_open: 1
items_waiting: 0
items_inactive: 2
items_archived: 0
Evidence: Edited post re-renders as the same single row with the moved date

Source: Edited post re-renders as the same single row with the moved date

# 14:2x observe Furniture post; render
new faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2
TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.HuMeZm/.lavish/today-2026-10-08.html rendered at 14:55 CEST
<summary>Updates (1)</summary>
<td>Aquablu Furniture limited release, 50 units - live on the partner webshop 15 Oct</td>

# +1h: edit moves the date; re-observe same ref; render
updated faa624f69df807f1fe803f18d08a8960003f029ffeddd6562ae6f74c7f7c30d2
TODO_RENDER: /var/folders/2c/9sf1hvpn23q336b08gs1np0c0000gn/T//fm-lab.HuMeZm/.lavish/today-2026-10-08.html rendered at 15:55 CEST
<summary>Updates (1)</summary>
<td>Aquablu Furniture limited release, 50 units - live on the partner webshop 22 Oct (moved)</td>
posted 14:21 CEST · slack-announcements (C_TEAM)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 1 info
  • ℹ️ bin/fm-channel-intake.sh:2021 - Two user-visible messages still describe the old routine-only retirement. The brief's 'Detection limits' line says routine traffic leaves after 86400s and that 'nothing ... corrected ever leaves', but updates now leave after 14 days, edited ones included. The to-do journal also records a retired update as 'the intake retired it as routine'. Behaviour is correct; only the wording is out of date.
  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An enrolled slack-announcements source becomes due on the 30-minute tick and claim hands it out with its kind ✅ pass live 01-intake-cli-transcript.txt: tick prints '2 source(s) due'; claim lists 'source: C_TEAM kind: slack-announcements'
Adversarial: a non-announcement (@mention) source trying to record --class update is refused ✅ pass live 01-intake-cli-transcript.txt: 'fm-channel-intake: --class update requires a slack-announcements source', exit=2
The Furniture announcement observed as an update is listed in the brief under 'What changed', never notified, and never on the to-do list ✅ pass live 01-intake-cli-transcript.txt: items shows class update; notify-due prints nothing; todo lists only the RMA ask; brief 'What changed' lists both updates
The day page shows a collapsed 'Updates' fold below Needs you now, newest post first, each with its post time and permalink ✅ pass live 04-day-page-updates-fold.png and 03-day-page.html: Updates (2), Furniture 'posted 14:21 CEST' with Open link above 'posted Wed 7 Oct 12:46 CEST'; the RMA ask stays in Needs you now
An edited announcement (moved webshop date) updates the same item and the same page row instead of adding a second one ✅ pass live 07-edited-post-on-page-transcript.txt: 'updated faa624…' (same key); Updates (1) row now reads '22 Oct (moved)'
Updates outlast the 1-day routine horizon, leave after 14 days, and an edited update leaves 14 days after its last change ✅ pass live 05-edit-and-retirement-transcript.txt: both open at +2d; price list inactive at +14d30m while edited Furniture stays open; both inactive at +15d; status items_inactive: 2
Retired updates drop off the day page and are not reported as closed work ✅ pass live 06-day-page-after-retirement.html has no Updates fold and no Closed fold; the data/todo journal shows superseded/source closures
The live orchestrator reads the real #channel-team on its 30-minute pass, keeps only launch/date/price/process posts per docs/channel-intake.md, and appends data/product-updates/log.md ⏸️ untested no Needs a live primary with the authenticated Slack connector reading real team channels, plus model judgement over real posts. The gate's lab primary cannot reach Slack, and sending real channel conten…
  • bash tests/fm-channel-intake.test.sh (all pass, including test_team_announcements_are_information_not_asks)
  • bash tests/fm-todo-render.test.sh (all pass, including test_team_updates_fold_newest_post_first)
  • Lab home via bin/fm-lab-home.sh create, with a mention source (slack-channel) and an announcement source (slack-announcements): fm-channel-intake.sh sources, tick, claim
  • Adversarial: observe --source C_MENTIONS --class update was refused with exit 2
  • observe --source C_TEAM --class update --source-epoch 1791462060 --link … for the Furniture post, plus an older price-list update and an obligation ask; then complete, items, notify-due, todo, brief
  • fm-todo-render.sh render, then a headless Chrome screenshot of the day page with the Updates fold opened
  • Edit: re-observing the same ref at +1h returned updated &lt;same key&gt;
  • Time-travel ticks with FM_CHANNEL_INTAKE_NOW at +2d, +14d30m and +15d, with items --state open|inactive and status
  • Render at +15d: no Updates fold, no Closed entry; the journal shows superseded/source closures
  • Second lab home: render, edit the post to move the date, re-render, and confirm one row now carries the new date
✅ **Document** - passed

✅ No issues found.

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

✅ No issues found.

@Amplify-Logic
Amplify-Logic merged commit 0e26a46 into main Oct 8, 2026
23 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