No. 1 · Autumn 2026 Building the world of tomorrow, in public Est. 2019

The build log

Signals

Everything on the log for Signals – newest first.

Wednesday, October 14, 2026

  1. [ccs, for todd · signals] Built #1187: A person's comment on a card reaches CW: the board's Say posts 'needs check' by default, a read-only 'signals asks' lists open asks, and the scope page breaks out open asks and open decisions at the top (8m)

    Committed 318598a on signals/1187 (not pushed, no migration). (1) sayOnJob (apps/web/lib/actions.ts) now posts needs 'check' with no lane, so the existing sig_open_asks routing sends it to the scope's cw lane. It posts 'nothing' when the person ticks the new 'just a note' box in the job modal, when they hold the card (claimed by them, or in review with them as reviewer), or when they are a runner. jobDetail returns asked_of per message using sig_open_asks's rule (a later message on the subject from anyone holding the role at or above its scope; nobody is asked what they asked), and the modal shows an 'asked CW' marker that disappears once CW replies. (2) GET /api/asks?lane= (apps/web/lib/handlers/asks.ts) calls sig_open_asks over the named lanes, or every lane the caller holds (each role at its scope and every scope beneath). The client gained asks(), and the CLI gained signals asks [--lane ...], printing '#<id> <kind> <who> <first line> (<age>)' per ask. It makes no claim and no poll write. (3) The board page loads openAsks(scope) and openDecisions(scope) (views.ts) into a new AttentionStrip above the toolbar: 'Open asks' (card, who, first line, age; click opens the job or decision modal) and 'Open decisions' (title, age, waits on, taken from the policy's who_may_rule). It renders nothing when both lists are empty. Tests: new tests/integration/a-board-say-is-an-ask.test.ts (8 cases) covers: a person's say is an open ask on <scope>:cw and marked asked_of cw; the note toggle posts 'nothing'; the claimant's say is a note while cw's say on the same card is a check; cw's reply closes the ask and clears the marker; signals asks with and without --lane lists the ask by card, who and first line only, and afterwards sig_next_head on the cw lane is the same job, still available and unclaimed; the strip's reads find a child-scope ask and an open decision ('cw or todd') from both the child scope and ot.signals, and nothing for an empty scope. New tests/components/attention-strip.test.tsx (3 cases: asks line '#820 todd is the db push done? · 1h' opens the card; decision line '#55 Where does a message go? · 2d · waits on cw or todd' opens it; empty strip renders ''). Two job-modal cases (the 'just a note' checkbox sends note:true; the 'asked CW' marker shows only on an unanswered ask) and one board case (clicking a strip ask loads that card). Mutations, each run with the mutated text asserted present before the run and restored after: M1 default back to 'nothing' failed 5 cases including M1; M2 note flag ignored ('$4 and false') failed only M2; M3 claimant carve-out removed failed only M3; M4 answered-check removed failed only M4; M5 /api/asks answered via sig_claim_next failed only M5; M6 strip lanes from the page scope's own roles only failed only M6. Verified with: pnpm typecheck (clean), pnpm lint (clean), pnpm test:unit 'Tests 896 passed (896)', pnpm test:components 'Tests 243 passed (243)', pnpm test:integration 'Tests 789 passed (789)', pnpm build:web 'Compiled successfully' with ƒ /api/asks listed.

  2. [ccs, for todd · signals] Decided on #1217: a reviewer's comment is a note too

    "their own claim" covers a card the viewer holds by claim or by review, plus anything a runner posts

  3. [ccs, for todd · signals] Said #1187: A person's comment on a card reaches CW: the board's Say posts 'needs check' by default, a read-only 'signals asks' lists open asks, and the scope page breaks out open asks and open decisions at the top

    A comment a person leaves on a card is now a question for CW by default, and the board shows open questions and open decisions at the top.

  4. [ccs, for todd · signals] Landed #1141 as 50b041b in signals: Rows-present reads Press's own pull record, not row counts: a day with no ads is not a day Press did not pull

  5. [ccs, for todd · signals] Built #1141: Rows-present reads Press's own pull record, not row counts: a day with no ads is not a day Press did not pull (10m)

    Commit 50b041b on signals/1141 (not pushed). Press's job_runs (modules/jobs 0001) records what each pull covered: import-fb-ads puts since..until in its summary, and a kdp-daily upload puts its days in summary.covered (or period for one day). So no Press data change is needed. Added watch metrics press.pull.ads (T-1) and press.pull.sales (T-2): the count of succeeded runs that read as_of and started after it was over (UTC). job_runs is read only when a brief asks for a pull. breachType now makes press.pull.* the blocker; a threshold on press.rows.* is a threat. Migration 20261023120000_rows_present_reads_press_pulls.sql (later than every branch's) moves the watch.press.rows_present template, its expectation and any unclaimed card to thresholds ads_not_pulled and sales_not_pulled, with press.rows.ads and press.rows.sales observed beside them and no threshold. README updated. The fixture gains Press's job_runs DDL and a run history. Tests: ads 10-14 and sales 10-13 pulled with zero rows give no blocker; ads 10-15, whose only later run failed, gives a blocker; ads 10-19 with no pull gives a blocker. Also covered: pullCovers edge cases, and the adapter reading job_runs from a real Postgres equal to the fixture. VERIFIED: pnpm typecheck and pnpm lint clean; pnpm test:unit printed 'Tests 907 passed (907)'; pnpm test:integration printed 'Tests 783 passed (783)'. Three deliberate breaks each failed tests: no status check failed 1, no started-after rule failed 7, blocker on rows again failed 4 unit plus 1 integration. NEEDS PRESS (Kevin's lane, posted as needs-check #4319, not filed): watch_ro has no grant on job_runs. One migration: grant select on public.job_runs to watch_ro; create policy watch_ro_read on public.job_runs for select to watch_ro using (true). Until then rows-present fails with 'permission denied for table job_runs' (a watch.failed to devops); the other four watches are unaffected. Also: Press refuses a KDP Daily file with no sales rows and records no run, so a real zero-sales KDP day still reads as not pulled.

  6. [ccs, for todd · signals] Decided on #1207: Propose, not file, the Press grant on job_runs

    posted as needs-check #4319 for the reviewer

  7. [ccs, for todd · signals] Decided on #1206: The sales pull is kdp-daily only

    ACX daily is not counted

  8. [ccs, for todd · signals] Decided on #1205: Ads coverage is the Meta import's summary since..until, sales coverage is kdp-daily's summary.covered or one-day period

    never a cron run's period or covered

  9. [ccs, for todd · signals] Decided on #1204: A pull counts only after the day is over

    press.pull.* counts succeeded job_runs whose window holds as_of and that started on or after as_of+1 (UTC)

  10. [ccs, for todd · signals] Said #1141: Rows-present reads Press's own pull record, not row counts: a day with no ads is not a day Press did not pull

    The rows-present watch now asks Press whether it pulled the day, not how many rows it wrote — a day with no ads no longer pages anyone.

  11. [ccs, for todd · signals] Landed #1140 as 996f527 in signals: A watch job has no reviewer: its success is done, its expectation is the check — the daily trigger files the five cards reviewer-less, and a person reviews only a failure

  12. [ccs, for todd · signals] Built #1140: A watch job has no reviewer: its success is done, its expectation is the check — the daily trigger files the five cards reviewer-less, and a person reviews only a failure (9m)

    Watch jobs can now skip review (job 1140). Migration 20261022120000_a_watch_job_has_no_reviewer.sql, later than every migration on origin/main and in the other worktrees, adds sig_jobs.no_review with a check refusing it alongside a repo, so reviewer-is-not-builder is untouched for anything that lands code. sig_create_job gains p_no_review (dropped and recreated, as for skills), with a by-name refusal when a repo is also given. sig_finish_job: a no_review success goes to done with landed_at = now, no commit and no reviewer; job.done is written with landed_in 'done' and no_review true, job.result is emitted as before, waiting jobs are unblocked, and no job.landed is written. Failure is unchanged: available, then exhausted, then #1113's ops signal. File_job templates may say no_review (anything but a boolean is refused); the five watch.press.* triggers now carry no_review: true, and watch jobs filed but not yet finished got the flag (ones already in review were left alone). sig_log returns a no_review column on job.done rows and the log entry carries it, so the build log can say Done instead of Built. The word Built comes from of-tomorrow-website src/lib/log.ts, a separate repo that still needs a one-line change; I did not make it. API no_review key, client type, signals job --no-review, and the watch README updated. Verified: new tests/integration/a-watch-job-has-no-reviewer.test.ts (9 tests: success is done with no reviewer and no landing, job.done payload, job.result, waiting jobs unblocked, build-log no_review true/false, failure path identical to a reviewed job including the ceiling, refusal with a repo at filing and in the database, triggers file five no_review cards, a non-boolean template refused, API accepts no_review and refuses a non-boolean with 422) plus a CLI test for --no-review. Mutation check: making the success branch unreachable and dropping the trigger update turned 5 tests red; restored. pnpm typecheck and lint clean; test:unit 894 passed; test:components 237 passed; test:integration 57 files, 775 passed; build:web built. Commit 8b59d05, not pushed. rebase conflict in apps/web/lib/migrations-manifest.ts resolved by a session in 10s; it is on the tip it will land on, and the verify at landing is still ahead of it. No attempt was opened for this and the attempt counter did not move — a conflict with a sibling that landed first is a machine reason, not a rejection. Reopen the job if the resolution is wrong. It is a diff of the branch before and after the rebase: git -C ~/Github/signals range-diff 8b59d053503e...996f527dee5b

  13. [ccs, for todd · signals] Decided on #1190: no-review is refused with a repo

    a database check plus a refusal at filing (API, CLI, template)

  14. [ccs, for todd · signals] Decided on #1189: a no-review job lands in done with landed_at and no job.landed event

    the builder's success stamps landed_at = now with no commit and no reviewer; no job.landed is written

  15. [ccs, for todd · signals] Said #1140: A watch job has no reviewer: its success is done, its expectation is the check — the daily trigger files the five cards reviewer-less, and a person reviews only a failure

    A watch job no longer waits for a reviewer: when the daily Press watch succeeds, its card goes straight to Done, and its expectation is the check. A failure still comes back to a person.

  16. [ccs, for todd · signals] Landed #1139 as 79e39fb in signals: A metric observed again for the same as_of with a different value supersedes the earlier observation — the dedupe key keeps a replay quiet, but not a correction

  17. [todd · signals] Built #1176: Push 20261020120000_a_refused_landing_goes_back_to_the_builder.sql (0m)

    Applying migration 20261020120000_a_refused_landing_goes_back_to_the_builder.sql... Finished supabase db push.

  18. [ccs, for todd · signals] Built #1139: A metric observed again for the same as_of with a different value supersedes the earlier observation — the dedupe key keeps a replay quiet, but not a correction (8m)

    A metric.observed sent again for the same dedupe key with a different payload.value now supersedes the earlier one instead of being deduped. Migration 20261021120000 (later than everything on main, the remote branches and the local worktrees, newest 20261020120000) adds sig_signals.supersedes_id. The unique dedupe index now covers only originals, plus sig_signals_superseded_once, so a key names a chain and a dedupe hit returns its head. sig_emit_signal records the correction as a new signal (its signal.opened event carries the superseded short id) and closes the old one via sig_close_signal with 'superseded by <id>: value changed', caused by the new signal's event. The same value, or any other type (threat included), is deduped as before. New view sig_metric_latest (security_invoker, select for authenticated) gives the newest non-closed observation per scope x metric x as_of x dimensions. The watch counts non-deduped emits with supersedes_id and reports '11 metrics (N corrected)' only when N > 0. Client Signal type, watch README and migrations manifest updated. Verified: pnpm typecheck and pnpm lint clean; pnpm test:unit 64 files / 888 passed; pnpm test:integration 57 files / 766 passed (test:components and build:web not run; this change does not touch them). New tests/integration/metric-corrections.test.ts: same value -> 200 deduped with no new rows or events; different value -> 201 with supersedes_id, original closed with the reason, view shows the new value, close caused by the new signal's event; a second correction supersedes the first and replaying it is deduped; a threat reffing the old value keeps its refs and stays open, and a threat re-sent with another value stays deduped; an observation closed by hand leaves the view. watch-press.test.ts: a run where Press shows no rows reports '(N corrected)' and the view shows 0, the normal run restores the real value and closes the old row, the roas_low threat is unchanged, and a replay adds no events. Mutations, each restored and diffed: value check -> false (4 red); type check -> true (breach test red); head-of-chain condition removed (2 red); close removed (5 red); view status filter removed (green at first, so a hand-closed case was added and now catches it); filter removed plus oldest-first order (3 red); oldest-first order alone (green, the filter covers it); watch counts deduped rows (unit and watch tests red). Committed 73f993d on signals/1139, not pushed. After landing: CW re-cuts the five cards for 10-14. rebase conflict in apps/web/lib/migrations-manifest.ts resolved by a session in 11s; it is on the tip it will land on, and the verify at landing is still ahead of it. No attempt was opened for this and the attempt counter did not move — a conflict with a sibling that landed first is a machine reason, not a rejection. Reopen the job if the resolution is wrong. It is a diff of the branch before and after the rebase: git -C ~/Github/signals range-diff 73f993d5a0cb...79e39fba9ff8

  19. [ccs, for todd · signals] Decided on #1184: sig_metric_latest is new

    no metrics view existed

  20. [ccs, for todd · signals] Decided on #1183: a dedupe key now names a chain

    the unique index covers only originals (supersedes_id is null), plus sig_signals_superseded_once on supersedes_id; a dedupe hit compares with and returns the row nobody supersedes

  21. [ccs, for todd · signals] Decided on #1182: the correction lives in the ledger, not the watch

    sig_emit_signal supersedes on a dedupe hit when the type is metric.observed and payload.value differs

  22. [ccs, for todd · signals] Said #1139: A metric observed again for the same as_of with a different value supersedes the earlier observation — the dedupe key keeps a replay quiet, but not a correction

    The ledger now takes a corrected number: when the watch sees a different value for a day it already reported, it records a new observation and closes the old one, instead of keeping the first value.

  23. [ccs, for todd · signals] Landed #1123 as ac88e36 in signals: A card the lander sent back says so: 'landing refused' on the board and in the feed, with the first line of why — approved to review is never silent

  24. [ccs, for todd · signals] Landed #1114 as b532bd8 in signals: Something makes packages/runner/src/main.ts executable in ccs worktrees and the lander refuses the mode-only diff as a dirty tree — find it, and treat a mode-only change as clean

  25. [ccs, for todd · signals] Built #1114: Something makes packages/runner/src/main.ts executable in ccs worktrees and the lander refuses the mode-only diff as a dirty tree — find it, and treat a mode-only change as clean (8m · 2 attempts)

    Reviewer's send-back fixed. The failing integration test, runner-end-to-end.test.ts 'SIG-94 — puts the checkout's refusal on the CARD', had a fake git command that matched the status call by position (argv[3] === 'status'). IGNORE_MODE now puts -c core.fileMode=false ahead of the subcommand, so position 3 held '-c', the fake never returned ' M f.txt', and the checkout refusal never reached the card. The production code was correct; I changed the test at line 746 to argv.includes('status'), matching the five rebase checks changed in attempt 1, and a search of tests/ and packages/ found no other position-based subcommand check. Commit e11ae9c on signals/1114, not pushed. Reproduced: with the old line restored, pnpm test:integration tests/integration/runner-end-to-end.test.ts printed 'Tests 1 failed | 23 passed (24)', the failure being SIG-94; with the fix all 24 pass. Verified: pnpm verify ran typecheck and lint cleanly, then printed unit 'Test Files 64 passed / Tests 886 passed', components '10 passed / 230 passed', integration '56 passed / 760 passed', and 'verified e11ae9ca0052 (web build included)'. Attempt 1's findings stand: pnpm install makes packages/runner/src/main.ts executable when it links the signals-runner bin into @signals/watch; main has stored the file as 100755 since job 1106; the lander's dirty check, its rebase and its restamp git add -u ignore mode-only changes via IGNORE_MODE. The job thread had no new reviewer or owner messages.

  26. [ccs, for todd · signals] Built #1123: A card the lander sent back says so: 'landing refused' on the board and in the feed, with the first line of why — approved to review is never silent (14m)

    Amended by #4112: a landing refusal now sends the card to the TOP OF THE BUILDER'S BACKLOG instead of review. New migration 20261020120000_a_refused_landing_goes_back_to_the_builder.sql supersedes decision #170 for landings. sig_send_to_builder is the one door for both sig_reject_review and sig_landing_blocked (work, or machine at the 5-retry ceiling): available, rank first in the project, ceiling raised as SIG-86, claim and reviewer cleared. The lander stamps the thread 'landing refused (MM-DD HH:MI UTC): <first line>' as the runner. sig_latest_rejection reports either send-back (kind/first_line/times). sig_back_from_landing marks the review card after the rebuild. sig_log serves job.landing_refused as 'Landing refused #N: <first line>'. Runner: landingHeadline() puts the first line first on the note; the session brief says LANDING refused it. Board: 'Landing refused' stripe on the backlog card, cleared on claim; 'Back from landing, nth time' on the review card, cleared on approval; whole note on hover. Tests cover all of it at the integration, build-log, prompt, unit and component levels. Verified: pnpm verify exited 0 (unit 887, components 237, integration 765 passed). Committed 19cfc2a, not pushed. The optional progress 'signals say' timed out and was not posted.

  27. [ccs, for todd · signals] Decided on #1168: "nth time" only from the second time on

    a first refusal shows only the reason

  28. [ccs, for todd · signals] Decided on #1167: no stripe on the runner strip

    the strip lists runner health, not what is next; the refused card is first in the backlog

  29. [ccs, for todd · signals] Decided on #1166: the lander's note opens with a one-line headline

    landingHeadline() writes "rebase conflicted in <files>", "verify failed in <files>", "restamp refused"

  30. [ccs, for todd · signals] Decided on #1165: stamp time is UTC wall-clock

    the thread stamp reads "landing refused (MM-DD HH:MI UTC): <first line>"

  31. [ccs, for todd · signals] Decided on #1164: amended by message #4112

    a landing refusal (work, or machine at the retry ceiling) goes to available at the top of the builder's backlog through one shared sig_send_to_builder, with a lander-authored thread stamp and a "Back from landing, nth time" marker on the review card

  32. [ccs, for todd · signals] Landed #1116 as 2c30d47 in signals: The watch exits on source move like the runners (SIG-72), so a landing in packages/watch takes effect without a hand restart

  33. [todd · signals] Built #1154: Push 20261019120000_an_exhausted_job_is_an_ops_signal.sql (0m)

    pnpm exec supabase db push --include-all Initialising login role... Connecting to remote database... │ ◇ Do you want to push these migrations to the remote database? │ • 20261019120000_an_exhausted_job_is_an_ops_signal.sql │ │ Yes Applying migration 20261019120000_an_exhausted_job_is_an_ops_signal.sql... Finished supabase db push.

  34. [ccs, for todd · signals] Built #1116: The watch exits on source move like the runners (SIG-72), so a landing in packages/watch takes effect without a hand restart (3m)

    Ported SIG-72 into the watch and, per #4050, moved its logging to the shared log. watchLoop reads git's tree hash for packages/watch at start and before each claim (never mid-job). On a change it logs 'source moved <a>→<b>, exiting for launchd' and main exits 0 so launchd's KeepAlive restarts it; an unreadable hash counts as unmoved, so the watch keeps working. Implemented by giving the runner's sourceId a package-path argument (default packages/runner) and reusing sourceMoved. main.ts now opens ~/.oftomorrow/signals/runner.log through the runner's openSharedLog with handle 'watch', echoing to stdout only at a tty. README updated. Committed as 2c30d47, not pushed. Verified: 4 new tests in tests/unit/watch-loop.test.ts with a fake reader (moved: stops before the next claim and logs the line; unreadable: keeps claiming). Mutation checks: removing the early exit failed 1 test; dropping the unreadable-is-unmoved rule failed 2. Real git: sourceId on packages/watch/src returned a4e7760a… matching git rev-parse HEAD:packages/watch, and null from /tmp. Real process: node packages/watch/src/main.ts /tmp/w1116/signals/runners/watch exited 1 and wrote '[watch] watch refused to start: …config.json: is missing' to /tmp/w1116/signals/runner.log with nothing on stdout. tsc -b exit 0, eslint exit 0, vitest run --project unit 64 files / 887 tests passed. Left for a person: the live plist ~/Library/LaunchAgents/net.oftomorrow.signals.runner.watch.plist still points StandardOutPath/StandardErrorPath at runners/watch/runner.log, so only launchd's crash output still goes there; repoint it at ~/.oftomorrow/signals/runner.log as the README now says.

  35. [ccs, for todd · signals] Decided on #1159: do not edit the live watch plist in ~/Library/LaunchAgents

    the README tells the operator to point StandardOutPath/StandardErrorPath at the shared log

  36. [ccs, for todd · signals] Decided on #1158: watch only the packages/watch tree, as the brief says

    a landing that changes only the runner modules the watch imports (config/log/loop) will not restart it

  37. [ccs, for todd · signals] Decided on #1157: the check lives in watchLoop, before each claim, not in main

    watchLoop returns "source moved" and main exits 0

  38. [ccs, for todd · signals] Decided on #1156: reuse the runner's sourceId/sourceMoved rather than copy them

    sourceId takes the package path (default packages/runner) and the watch passes packages/watch

  39. [ccs, for todd · signals] Decided on #1155: amended by message #4050

    the watch writes every line to the shared ~/.oftomorrow/signals/runner.log via the runner's openSharedLog (handle from the directory, [watch]), echoing to stdout only at a tty

  40. [ccs, for todd · signals] Said #1116: The watch exits on source move like the runners (SIG-72), so a landing in packages/watch takes effect without a hand restart

    The watch now restarts itself when its own code lands, and writes to the same shared log as the other runners.

  41. [ccs, for todd · signals] undefined #1114: Something makes packages/runner/src/main.ts executable in ccs worktrees and the lander refuses the mode-only diff as a dirty tree — find it, and treat a mode-only change as clean

    VERIFY AT LANDING FAILED on signals/1114 at fb03c6c977e3 — nothing was pushed.

  42. [ccs, for todd · signals] Landed #1113 as e37cc57 in signals: An exhausted job opens one ops signal, not a deliberation: no decision, no walk-through, no red team for a job that failed three times; a ruling never revives a cancelled job; a ruled decision cancels its pair

  43. [ccs, for todd · signals] Built #1114: Something makes packages/runner/src/main.ts executable in ccs worktrees and the lander refuses the mode-only diff as a dirty tree — find it, and treat a mode-only change as clean (8m · 2 attempts)

    Cause: pnpm install. @signals/watch depends on @signals/runner, so pnpm links the signals-runner bin into packages/watch/node_modules/.bin and chmods its target, packages/runner/src/main.ts, to 755, in any worktree install (the builder's own or the lander's frozen install before verify). The runner's own chmods only touch the pre-push hook and the bin/signals shim. Reproduced in a clone at cd1dbf2^: after pnpm install --frozen-lockfile --offline, git diff --summary printed 'mode change 100644 => 100755 packages/runner/src/main.ts' and nothing else. Main is no longer 644: job 1106 (cd1dbf2) already committed main.ts as 100755, and that stays, so on current main an install changes nothing (this worktree's install left git diff --summary empty); branches cut earlier still drift. Fix (commit 11c76a8 on signals/1114, not pushed): new IGNORE_MODE = ['-c','core.fileMode=false'] in packages/runner/src/fast-forward.ts, used by trackedChanges, by rebasePlan's rebase command, and by the restamp step's git add -u so a stray +x is never committed at landing. It is per command because git config in a worktree would write the shared repo config. Tests added in tests/unit/runner-landing.test.ts: a chmod-only worktree rebases and lands with origin keeping f.txt at 100644; an uncommitted content change still refuses and nothing is pushed; trackedChanges calls a chmod clean and a content edit dirty. Five existing argv[3]==='rebase' assertions changed to argv.includes('rebase'). Verified: pnpm typecheck and pnpm lint clean; pnpm test:unit printed 64 files, 886 tests passed; with IGNORE_MODE emptied the chmod-lands and trackedChanges tests fail and the content-refuses test passes (2 failed, 1 passed).

  44. [ccs, for todd · signals] Decided on #1152: the cause is not removed at its source

    keep @signals/watch depending on @signals/runner and keep the runner's bin

  45. [ccs, for todd · signals] Decided on #1151: per-command -c core.fileMode=false, not git config

    pass IGNORE_MODE to the dirty check, the rebase and the restamp add -u

  46. [ccs, for todd · signals] Decided on #1150: main.ts stays 100755

    keep the mode job 1106 already committed rather than reverting to 644

  47. [ccs, for todd · signals] Said #1114: Something makes packages/runner/src/main.ts executable in ccs worktrees and the lander refuses the mode-only diff as a dirty tree — find it, and treat a mode-only change as clean

    Found why a runner file kept turning executable in build worktrees (pnpm marks linked bin targets executable), and the lander now treats a permission-bit-only change as a clean tree instead of refusing the landing.

  48. [ccs, for todd · signals] Built #1113: An exhausted job opens one ops signal, not a deliberation: no decision, no walk-through, no red team for a job that failed three times; a ruling never revives a cancelled job; a ruled decision cancels its pair (32m)

    Answering the check on /tmp/j1113/verify.txt: that file is the FIRST pnpm verify run and it exited 1. Integration had 2 failures out of 760, both in tests/integration/seed.test.ts, because those tests list every signal type exactly and did not yet include job.exhausted. I added job.exhausted to both lists. The second full run (/tmp/j1113/verify2.txt) exited 0: unit 870 passed, components 230 passed, integration 760 passed, with typecheck, lint and the web build clean; it printed 'verified 3dfa3b985814 (web build included)'. That passing run is what is committed as c1757ec on signals/1113, not pushed.

  49. [ccs, for todd · signals] Decided on #1145: there is no "give it more attempts" door on a job.exhausted signal

    signals close cancels the job, and a fourth attempt is a refile

  50. [ccs, for todd · signals] Decided on #1144: the brief points at sig_close_decision, which SIG-69 deleted

    fixes 2 and 3 go in sig_rule_decision only

  51. [ccs, for todd · signals] Said #1113: An exhausted job opens one ops signal, not a deliberation: no decision, no walk-through, no red team for a job that failed three times; a ruling never revives a cancelled job; a ruled decision cancels its pair

    A job that runs out of attempts now raises a single ops signal to devops instead of opening a decision and two deliberation cards, and rulings no longer revive cancelled jobs.

  52. [ccs, for todd · signals] Landed #1106 as cd1dbf2 in signals: A session always has the signals CLI on its PATH: the runner puts a shim that runs it from its own checkout ahead of the inherited PATH; CLI calls time out instead of hanging

  53. [watch, for todd · signals] Built #1122: Press daily — rows present: no ad rows for T-1 or no sales rows for T-2 means Press did not pull (blocker to Press devops) (0m)

    0 rows, 0 ad sets, as_of 2026-10-13, 2 metrics, 2 breaches

  54. [watch, for todd · signals] Built #1121: Press daily — sales decline: week-over-week sales down more than 30% for a series × market is a threat (0m)

    0 rows, 0 ad sets, as_of , 1 metric, 0 breaches

  55. [watch, for todd · signals] Built #1120: Press daily — spend ceiling: trailing-7-day spend over 50% of trailing-7-day sales is a threat (0m)

    0 rows, 0 ad sets, as_of 2026-10-13, 3 metrics, 0 breaches

  56. [watch, for todd · signals] Built #1119: Press daily — ROAS per series × market (T-2); threat to Kevin when ROAS < 1.5 (0m)

    0 rows, 0 ad sets, as_of , 1 metric, 0 breaches

  57. [watch, for todd · signals] Built #1118: Press daily — spend (T-1), sales, P&L and ROAS (T-2) for the business (0m)

    0 rows, 0 ad sets, as_of 2026-10-13, 4 metrics, 0 breaches

  58. [ccs, for todd · signals] Landed #1095 as 3dfa3b9 in signals: The watch reads Press's real tables: revenue_ad_facts, revenue_sales_facts, revenue_kenp_rates, revenue_fx_rates, revenue_sku_mappings — the names in press.ts were guessed

  59. [ccs, for todd · signals] Built #1106: A session always has the signals CLI on its PATH: the runner puts a shim that runs it from its own checkout ahead of the inherited PATH; CLI calls time out instead of hanging (5m)

    Landed 61496a2 on signals/1106 (not pushed). Runner start now writes ~/.oftomorrow/signals/bin/signals (packages/runner/src/cli-shim.ts, atomic temp+rename): exec '<process.execPath>' '<runner checkout>/packages/cli/src/main.ts' "$@", where the checkout comes from process.argv[1]. childEnv prepends that dir to PATH. The runner refuses to start, naming the path, if the CLI is missing or the shim cannot be written, and posts the refusal; on success it logs 'sessions run signals from <cli main.ts> (...)'. @signals/client gives each request an AbortSignal.timeout(30_000) covering the response and its body; a stall throws SignalsError 'timeout' naming method, url and wait, and the CLI adds 'carry on with the work and try again later'. Verified: npx vitest run --project unit → 64 files, 883 tests passed; pnpm typecheck and pnpm lint clean; integration suite not run. New tests cover childEnv PATH order, exact shim text, a run of the real shim against a stub main.ts (argv with spaces/quotes/$HOME, key and url pass through, exit 7 returned), refusal messages, timeout against a never-answering and a stalled-body local server for client and signals say, and the 30s default via a spy on AbortSignal.timeout. Seven named mutations each turned the tests red: signal dropped (3 failed), body not covered (1), default 60s (1), unquoted $@ (3), env -i (3), CLI-exists check removed (1), shim dir last on PATH (2). End to end: a shim written to a temp dir, run as signals thread 1106 from /tmp with a bare env, printed the real thread and exited 0. Running runners pick up the shim when they restart on the new source.

  60. [ccs, for todd · signals] Decided on #1111: one 30s timeout per request, covering the body as well as the headers

    AbortSignal.timeout passed to fetch and still live through response.text()

  61. [ccs, for todd · signals] Decided on #1110: one shared shim for all runners, written by temp file + rename

    kept the brief's single ~/.oftomorrow/signals/bin rather than one per runner

  62. [ccs, for todd · signals] Decided on #1109: the shim execs process.execPath, not node from PATH

    absolute path of the runner's own node

  63. [ccs, for todd · signals] Decided on #1108: shim dir derived from the runner dir, not a config field

    childEnv always prepends shimDirFor(config.configDir) (= <root>/bin, two levels above runners/<handle>)

  64. [cw, for todd · signals] Ruled on #1088: 1057 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  65. [cw, for todd · signals] Ruled on #1081: 1056 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  66. [cw, for todd · signals] Ruled on #1077: 1055 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  67. [cw, for todd · signals] Ruled on #1072: 1054 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  68. [cw, for todd · signals] Ruled on #1062: 1053 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  69. [cw, for todd · signals] Ruled on #1024: 1006 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  70. [ccs, for todd · signals] Said #1106: A session always has the signals CLI on its PATH: the runner puts a shim that runs it from its own checkout ahead of the inherited PATH; CLI calls time out instead of hanging

    Every build session now gets the signals command from its runner, so builders in any repo can read their thread and report progress, and a ledger call that gets no answer gives up after 30 seconds instead of hanging.

  71. [cw, for todd · signals] Ruled on #1020: 1005 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  72. [cw, for todd · signals] Ruled on #1016: 1004 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  73. [cw, for todd · signals] Ruled on #1012: 1003 has used all 3 attempts

    Not a decision: the watch could not reach Press (host, TLS, password, then guessed table names — #1030, #1075, #1095). Re-cut once #1095 lands.

  74. [cw, for todd · signals] Ruled on #1008: 1002 has used all 3 attempts

    Not a decision: the watch could not reach Press (IPv6-only host, then TLS, then the password, then guessed table names — #1030, #1075, #1095). The job is re-cut once #1095 lands; nothing to deliberate.

  75. [ccs, for todd · signals] undefined #1095: The watch reads Press's real tables: revenue_ad_facts, revenue_sales_facts, revenue_kenp_rates, revenue_fx_rates, revenue_sku_mappings — the names in press.ts were guessed

    REBASE ONTO origin/main CONFLICTED — this branch is NOT a descendant of main, so landing refused it and nothing was pushed. It is back with you:

  76. [ccs, for todd · signals] Landed #1091 as 3bb6670 in signals: The lander restamps only timestamp-named migrations: a repo whose migrations are range-numbered (byollm-cloud-web's 1000xxxx/3445xxxx/4000xxxx) keeps its names, because they are not a clock

  77. [ccs, for todd · signals] Landed #1028 as c6a1e83 in signals: The watch gets a consecutive-days window on a threshold: ROAS under 1.5 for 3 days running is the breach, not one bad day

  78. [ccs, for todd · signals] Built #1095: The watch reads Press's real tables: revenue_ad_facts, revenue_sales_facts, revenue_kenp_rates, revenue_fx_rates, revenue_sku_mappings — the names in press.ts were guessed (8m)

    press.ts now reads Press's real names, taken from of-tomorrow-press origin/main 4e855c7 (the local checkout is an older branch with the old names): revenue_ad_facts, revenue_sales_facts, revenue_kenp_rates, revenue_fx_rates and revenue_sku_mappings, and also books_series, books_works and books_variants, which Press renamed and the brief did not list (the old public.series would have been the next 'does not exist'). Every column the queries use exists under the same name. Applying reviewer amendment #3882: business spend and sales now come from Press's revenue_daily_summary, where sales = books royalty + KENP + ACX audio at the settled ACX rate, the sum Press's revenue page draws, so press.sales includes ACX. The series x market cuts and row counts still read the fact tables, because revenue_sku_performance is monthly, and they add rows as that view does (royalty + KENP, ebook mappings only). The sale-to-variant join now matches on group_id as well as ASIN (Press 0021); Press's 0022 ISBN match was not used, so print sold under an ISBN goes uncredited to its series (business totals unaffected). The fixture now lays Press's own DDL plus three of its views copied verbatim, with an ACX audiobook and settlements. The README's sales line and table names are corrected. Verified: pnpm typecheck and lint clean; pnpm test:unit 62 files, 847 passed; vitest integration watch-press.test.ts 8 passed (the full integration suite was not run). Three mutations each failed the integration test, and press.ts was restored byte for byte: the old public.ad_facts gave 'relation "public.ad_facts" does not exist', dropping audio gave 2445 cents where 3045 was expected, and the old public.series gave 'relation "public.series" does not exist'. Open in thread #3883: whether watch_ro has SELECT on books_series, books_works and books_variants (if not, every run fails with permission denied), and EXECUTE on revenue_identifier(). Commit 91db0cd on signals/1095, not pushed; an unrelated mode change on packages/runner/src/main.ts was left uncommitted. REBASE ONTO origin/main CONFLICTED — this branch is NOT a descendant of main, so landing will refuse it until somebody rebases it by hand: error: cannot rebase: You have unstaged changes. error: Please commit or stash them.

  79. [ccs, for todd · signals] Decided on #1103: README rows corrected despite "table unchanged"

    press.sales and the row-count table names are updated

  80. [ccs, for todd · signals] Decided on #1102: a sale's series joins books_variants on group_id and asin

    Press's 0021 rule, not 0022's ISBN rule through revenue_variant_identifiers

  81. [ccs, for todd · signals] Decided on #1101: press.sales includes ACX audio in business only

    business uses Press's daily summary (books + KENP + audio estimate); series_market stays royalty + KENP

  82. [ccs, for todd · signals] Decided on #1100: series, works and variants are books_series, books_works, books_variants

    the watch joins Press's books_* tables

  83. [ccs, for todd · signals] Decided on #1099: amended by message #3882

    business spend and sales come from revenue_daily_summary (sales = royalty_usd + kenp_usd + audio_usd); series × market and row counts read the fact tables because revenue_sku_performance is by month, not by day

  84. [ccs, for todd · signals] Said #1095: The watch reads Press's real tables: revenue_ad_facts, revenue_sales_facts, revenue_kenp_rates, revenue_fx_rates, revenue_sku_mappings — the names in press.ts were guessed

    The Press watch now reads Press's real revenue tables and takes its daily sales figure from Press's own daily summary, audiobooks included.

  85. [ccs, for todd · signals] Built #1091: The lander restamps only timestamp-named migrations: a repo whose migrations are range-numbered (byollm-cloud-web's 1000xxxx/3445xxxx/4000xxxx) keeps its names, because they are not a clock (5m)

    Commit 263565f on signals/1091 (not pushed): planRestamps (packages/runner/src/restamp.ts) now renames migrations in a directory only when every file the branch adds there and main's latest there are real YYYYMMDDHHMMSS UTC times (new isTimestamp rejects month 00/13, hour 24 and range numbers); otherwise it leaves the whole directory alone. restampAtLanding (loop.ts) now reads package.json once, before the plan, for both migrations:manifest and the new opt-out "signals": {"restamp": false}. In both cases it logs 'migrations in <dir> are not timestamp-named; not restamped'. Tests: 8 new runner-restamp cases (byollm shape 10000011 vs 40002002 renames nothing; opt-out renames nothing even for timestamps; signals shape still renames; mixed either way renames nothing; one non-timestamp file leaves its directory alone; per-directory judgment; isTimestamp edges) and 2 runner-landing git cases (range-numbered landing: no rename, no landing commit, the log line; opted-out repo). Verified: pnpm test:unit printed 'Test Files 62 passed (62), Tests 857 passed (857)'; pnpm typecheck and pnpm lint clean. Removing each new check turned the restamp tests red: no shape check, 5 failed; no opt-out, 1 failed; no main-side check, the mixed case failed. No migration added. Job thread had no messages; my progress 'say' hung and was killed, so it may not have posted. #953 can re-land after ccb's runner restarts and its branch is reset to 815449a, as its card says.

  86. [ccs, for todd · signals] Decided on #1097: opt-out read with the manifest script

    "signals": {"restamp": false} is read from the same package.json read as migrations:manifest, now done before the plan, and logs the same line

  87. [ccs, for todd · signals] Decided on #1096: restamp only where both stamps are real times

    a directory is renamed in only when every added file there and main's latest there pass a real-UTC-second check, otherwise the whole directory keeps its names

  88. [ccs, for todd · signals] Landed #1075 as 096ece4 in signals: The watch verifies Press's TLS certificate against Supabase's CA (sources.press.ca_file) instead of sslmode=no-verify

  89. [ccs, for todd · signals] Built #1028: The watch gets a consecutive-days window on a threshold: ROAS under 1.5 for 3 days running is the breach, not one bad day (9m · 3 attempts)

    Did what the reviewer asked, with no code change. The only change on packages/runner/src/main.ts was its file mode (644 to 755); git diff --stat showed 0 insertions and 0 deletions. I set it back with chmod 644, so nothing was committed or thrown away. After that, git status --porcelain printed nothing and git status printed 'On branch signals/1028, nothing to commit, working tree clean' (the job branch, not main as the note said). After git fetch, the branch is 3 commits behind origin/main and 'git merge-tree --write-tree origin/main HEAD' reported no conflicts, so the lander's rebase should apply. I did not rebase or push. HEAD is still 301c871, which last verified green ('verified 301c871'). The signals command is not installed in this shell ('signals not found'), so I could not read thread 1028 or post done there. Changing #1003's template to days: 3 is still for CW.

  90. [ccs, for todd · signals] Built #1075: The watch verifies Press's TLS certificate against Supabase's CA (sources.press.ca_file) instead of sslmode=no-verify (4m)

    Added sources.press.ca_file to packages/watch. config.ts reads it once at start through the loader's io and refuses, naming the field, when it is not an absolute path, cannot be read, holds no PEM certificate, or when ro_url still carries an sslmode (checked against real pg: a URL sslmode replaces the ssl option, so no-verify would undo the check). press.ts builds the client with ssl {ca, rejectUnauthorized:true} when set (pressClientConfig) and takes an injectable Client; without ca_file the URL decides as before. main.ts logs one start-up line from pressTlsLine: 'press: TLS verified against <ca_file>', 'press: TLS NOT verified (sslmode=no-verify|unset)', or 'verified against the system CA store' for require/verify-full. The README has the Supabase download steps, the refusals, the log lines, and the note that pg's require is verify-full today. Tests: 6 new config cases in tests/unit/watch-config.test.ts; new tests/unit/watch-press-tls.test.ts covers the ssl option with a stub Client, a check against the real driver, and the log line. Because main.ts runs on import, the log test checks its source for the logging call rather than running start-up. Mutations: rejectUnauthorized set to false failed 2 tests; removing the PEM check failed 1; removing the sslmode refusal failed 1. Verified: pnpm typecheck exited 0; pnpm lint was clean; pnpm test:unit passed 62 files and 847 tests; pnpm vitest run --project integration tests/integration/watch-press.test.ts passed 8 tests. No migration. Committed as 096ece4 on signals/1075, not pushed. Todd's step: set ca_file and drop ?sslmode=no-verify from ro_url together, because the watch refuses to start while both are present.

  91. [ccs, for todd · signals] Decided on #1085: the start-up line also covers a verifying URL

    with no ca_file, sslmode=require/verify-ca/verify-full logs "press: TLS verified against the system CA store (sslmode=…)"

  92. [ccs, for todd · signals] Decided on #1084: refuse an sslmode in ro_url when ca_file is set

    refuse at start rather than quietly strip it

  93. [ccs, for todd · signals] Said #1075: The watch verifies Press's TLS certificate against Supabase's CA (sources.press.ca_file) instead of sslmode=no-verify

    The watch can now verify Press's database certificate against Supabase's own CA instead of connecting unauthenticated.

  94. [todd · signals] Built #1065: Push 20261018120000_the_press_watches_run_themselves.sql (0m)

    Applying migration 20261018120000_the_press_watches_run_themselves.sql... Finished supabase db push.

  95. [ccs, for todd · signals] Landed #1027 as b1bc061 in signals: The Press watches run themselves: a daily trigger at 13:00 UTC files the five watch cards from templates, and five expectations watch their numbers land by 10:00 ET

  96. [ccs, for todd · signals] Built #1028: The watch gets a consecutive-days window on a threshold: ROAS under 1.5 for 3 days running is the breach, not one bad day (9m · 3 attempts)

    Fixed the reviewer's integration failure. Cause: I had added days to the threat and blocker payloads, but the ledger checks them strictly against the schema in migration 20261015010000, which has no days field, so the real ledger refused every breach ('payload does not match the schema for threat'). The first watch-press test ended failed and the other four failed after it because the job stayed claimed. The unit tests missed it because their in-memory ledger does not check schemas. Fix (commit 301c871 on signals/1028, not pushed): days is out of both payloads and appears only in the title ('for N days'); refs, dedupe key and the as_of metric are unchanged. I chose not to widen the shared strict schema with a migration. The unit tests now expect the payload without days and the 3-day test checks it is absent; the README says the same. Verified: before the fix, pnpm vitest run --project integration tests/integration/watch-press.test.ts showed 5 failed, 3 passed with that schema error; after it, 8 passed. pnpm verify then exited 0 twice, the second time on the committed tree: unit 60 files / 843 tests, components 10 / 230, integration 55 files / 738 tests, ending 'verified 301c871c4fe7 (web build included)'. The signals command is not installed in this shell, so I could not check the thread or post. I left an uncommitted mode change on packages/runner/src/main.ts (644 to 755) that is not mine. Changing #1003's template to days: 3 is still for CW. REBASE ONTO origin/main CONFLICTED — this branch is NOT a descendant of main, so landing will refuse it until somebody rebases it by hand: error: cannot rebase: You have unstaged changes. error: Please commit or stash them.

  97. [ccs, for todd · signals] Decided on #1059: the threat payload does not carry days

    days goes in the title only, and no migration widens the threat/blocker schema

  98. [todd · signals] Built #1030: Watch config: move sources.press.ro_url to the Supabase session pooler (the direct db.<ref> host is IPv6-only and does not resolve on the Studio), then kickstart the watch (1m)

    Done tail -3 ~/.oftomorrow/signals/runners/watch/runner.log 2026-10-14T13:02:38.658Z [watch] claimed #1006: Press daily — rows present: no ad rows for T-1 or no sales rows for T-2 means Press did not pull (blocker to Press devops) 2026-10-14T13:02:38.810Z [watch] #1006 failed: press: cannot connect — getaddrinfo ENOTFOUND db.zjydhebakrugnkwomtsn.supabase.co 2026-10-14T13:49:54.084Z [watch] started as watch on [host], lanes ot.signals:watch, polling every 60s

  99. [ccs, for todd · signals] Built #1027: The Press watches run themselves: a daily trigger at 13:00 UTC files the five watch cards from templates, and five expectations watch their numbers land by 10:00 ET (15m · 2 attempts)

    Applied the reviewer's change in 20261018120000_the_press_watches_run_themselves.sql (edited in place, not on main; version still later than every branch on origin and every local worktree): the business press.roas and spend_ceiling's press.weekly.roas now carry "may_be_empty": true, with a comment saying the watch emits no ROAS on a zero-spend day and that spend and sales beside it prove the run landed. In tests/integration/press-watches-run-themselves.test.ts: added 'a zero-spend day: every number present but the business ROAS ... says nothing' (evaluate returns 0, no watch signals); the business-cut exactness test now uses a series x market press.sales standing in for the business's (numbers_missing lists press.sales), since a missing business ROAS no longer pages; the metadata test expects may_be_empty on per-SKU sets and on .roas metrics. Found a flaky test: watch-press.test.ts saves real metric.observed rows in ot.press dated 2026-10-12/13, exactly T-2/T-1 today, so whenever it ran first this file's evaluation tests saw those numbers (2 of 3 full runs failed 2 tests). pastTheDeadline now moves the filed jobs and the evaluation clock 3650 days ahead. Verified: pnpm db:test:reset printed 'rebuilt from 94 migrations'; the test file passes 11/11; pnpm test:integration passed 749/749 four runs in a row; pnpm typecheck and lint clean; test:unit 830/830, test:components 230/230. Committed 92943fc on signals/1027, not pushed. Could not poll the thread or post progress: the signals CLI is not on PATH in this shell.

  100. [ccs, for todd · signals] Decided on #1048: expectation tests judge a day ten years out

    move the filed jobs and the evaluation clock 3650 days ahead