fix: act on CEO digest board comments same-tick instead of next week #435
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 21 minutes 48 seconds
Due date
igor
21 minutes 48 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!435
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/433-ceo-digest-steering-is-unreachable-the-footer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this PR does
ceo_digest_pending_steering_number-- selector for an open digest carrying a reviewer comment newer than the CEO's own last reply (the watermark; no new state needed)ceo_digest_thread,ceo_build_digest_steering_prompt,_ceo_parse_work,ceo_parse_digest_steering,ceo_file_digest_workinlib/ceo.shdo_ceo_tick(bin/tick.sh) that runs every tick (not week-gated), files any resulting work DIRECTLY as Agent-labeled/unassigned tickets (no second greenlight round-trip), and replies on the digest thread naming what it did -- digest stays open + assignedFORGEJO_REVIEWERcounts as steering, in both the pending-selector AND in what's rendered into the steering prompt (ceo_digest_threadfilters the thread to reviewer + CEO's own replies only) -- a comment from anyone else never reaches the modelbin/test-ceo.sh: selector matches a commented digest, does not match one the CEO already replied to, is not suppressed by the weekly ISO-week stamp, rejects a non-reviewer comment, and the thread-rendering itself excludes non-reviewer textContext
igor#433: the digest footer said "comment to steer" but nothing read a digest comment until the next week's composition (
ceo_prior_digest_steering, only called when composing a fresh digest) -- up to 7 days late, even though the CEO already had a same-day act-on-input path for board questions. This adds the same same-day path for digest comments, on the signal the footer already promised, so no footer text changed.A first attempt at this ticket was rejected in security review for gating the selector on "commenter is not the bot" rather than "commenter is the reviewer" -- that would have let any commenter inject work into the autonomous execution queue. This implementation gates on
.user.login == $reviewereverywhere a comment is read for steering, including the prompt-rendering step itself (not just the pending-check), and adds test coverage for a non-reviewer comment at both layers.Live verification note (per the issue):
joshtronic/idolidle.com#27is open with an unanswered board comment and should be picked up + replied to on the next CEO pass once this lands. Left open, not touched by this PR.Test plan
make testpasses (bin/check-sync.sh+ allbin/test-*.sh, includingbin/test-ceo.sh)make lintpasses (shellcheck+mdl)idolidle.com#27picking up a reply on the next real CEO pass, which is outside this PR's controlCloses #433
🤖 Review —
REQUEST_CHANGES(automated)CI for
aa9b5e4a: successAdds a same-tick "act on board steering left as a digest comment" path (Path 1c) plus five new helpers in
lib/ceo.sh. Scope is tight, the checklist matches the diff item-for-item, the reviewer-authorship gate is genuinely applied at both layers, and CI is green — but two correctness issues in the new code should be fixed before merge.Blocking
lib/ceo.shceo_digest_thread: nothing is ever markedNEWon the first steering round. The jq filter isif ($lb != "" and .created_at > $lb) then "NEW -- ". When the CEO has never replied on the thread,last_bis"", so the guard short-circuits and no comment gets theNEW --prefix — even though in that state every reviewer comment is unanswered. The prompt built byceo_build_digest_steering_prompttells the model "comments marked NEW -- are unanswered", so on the most common case (a fresh digest, first board comment, no CEO reply yet — i.e. exactly theidolidle.com#27scenario cited in the description) the model is shown a thread with zero unanswered items and may reasonably reply "nothing needed" and file nothing. Fix:if ($lb == "" or .created_at > $lb). Note the existing thread tests both include aBOT_USERcomment or don't assert onNEW, so this branch is untested — please add a case with reviewer-only comments asserting theNEW --prefix.bin/tick.shPath 1c: work is filed before the reply is posted, so a failed reply POST re-files duplicate tickets._ceo_file_digest_work "$repo" "$dparsed"runs first; if the subsequent_fj POST .../commentsfails, the code only logswarning: ceo: failed to post steering replyand returns 0. The watermark (BOT_USER's last comment) never moves, so the next tick re-selects the same digest, re-runs the model, and files the same work again — Agent-labeled and unassigned, i.e. straight into the autonomous execution queue.ceo_codecheck_proposalgates on the code, not on already-open CEO-filed tickets, so it won't catch the dupe. Either post the reply first and file work only on success, or dedupe against open issues carryingCEO_DIGEST_WORK_MARKERbefore filing. A test for the failed-reply path would be worth having.Non-blocking observations
ceo_digest_threadrenders each comment as a single"> [login] body"line; multi-line reviewer comments will only have the first line prefixed, and their subsequent lines land unquoted next to the prompt's own###section headers. Reviewer is trusted so this isn't a security issue, but it makes the thread section easy to visually corrupt (a comment containingNEW --or### The board threadwould blend in).BOT_USERcomment on the digest thread", not "any steering reply". If some other path ever comments on a digest as the bot, it will silently suppress pending steering._fjstub redefined at line ~325 ofbin/test-ceo.shpersists for everything after it in the file (theceo_read_metricsblock follows). It appears harmless here since that block doesn't hit the API, but it's a latent trap for the next test added below it.What's good
select(.user.login == $r)in the selector, and the reviewer/bot-only filter inceo_digest_thread), with tests at both — that addresses the prior security rejection properly.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.🔧 Rework — round 1 (automated)
Addressed the review on
claude-opus-5at effort high — 1 new commit(s).🤖 Review —
REQUEST_CHANGES(automated)CI for
e8a7a075: successAdds a same-tick "act on board comment on the open CEO digest" path (selector + thread render + parse + direct Agent-ticket filing + reply), wired as Path 1c in
do_ceo_tick. Scope is tight, the checklist matches the diff item-for-item, CI is green, and the reviewer-only authorization gate is genuinely applied at both layers (selector and prompt render) with tests for each. One concrete defect blocks.Blocking
lib/ceo.sh:ceo_digest_pending_steering_number— unsetBOT_USERturns this into an unbounded ticket-filing loop. The watermark is[ .[]? | select(.user.login == $b) | .created_at ] | last, with$bdefaulted to""via${BOT_USER:-}. IfBOT_USERis empty/unset at runtime, no comment ever matches,last_bstays empty, and the[ -z "$last_b" ]branch reports pending forever — even after the CEO's own reply lands. Every tick then: model call → reply posted → the same===WORK===blocks re-filed as Agent-labeled, unassigned tickets straight into the autonomous queue. That is precisely the failure mode the reply-first ordering comment inceo_commit_digest_steeringsays it exists to prevent, just reached by a different door. The three${BOT_USER:-}defaults in this diff suggest the author isn't certain the var is always set; I can't verify it from the diff either.[ -n "${BOT_USER:-}" ] || return 0(same shape as the existing[ -n "$reviewer" ] || return 0) — and add abin/test-ceo.shcase asserting the selector returns empty whenBOT_USERis unset. The tests currently setBOT_USER="igor"unconditionally, so this hole is invisible to the suite.Non-blocking, worth a look
lib/ceo.sh:ceo_commit_digest_steering— stdin sharing in the work loop.while IFS= read -r w; ... done < <(jq -c '.[]?' ...)callsceo_codecheck_proposal(a model-backed gate) inside the loop body. If that call — or anything under it — reads stdin, it eats the remaining records and the second work item is silently dropped, no log line. Worth< /dev/nullon the codecheck/file calls, or reading the array into a var first. (May be pre-existing pattern from the proposal path; flagging because the cap is 2 and the loss would be silent.)BOT_USERon the digest thread after a reviewer comment — from this path or any other automation — permanently suppresses that steering with no log line. Consider anchoring the watermark on a marker the CEO stamps into its steering replies rather than "any bot comment".bin/test-ceo.sh— stubs that outlive the block._CC_ORIG/ceo_codecheck_proposalis carefully restored, butlog() { :; }andforgejo_add_label()are redefined and never restored, and they shadow for the rest of the file (ceo_read_metricsand everything after). CI is green so nothing currently depends on them, but this is a latent way to silently neuter a later assertion. Same treatment as_CC_ORIGwould be cheap.ceo_build_digest_steering_prompthas no direct test. Everything around it is covered; the prompt builder itself (including that the thread is actually interpolated) is not.[[ "$last_r" > "$last_b" ]]is a lexical compare — correct for Forgejo'sZ-suffixed RFC3339, but would silently misorder if the API ever returns offset-form (+00:00) timestamps. Fine as-is; noting it.Things I checked and am satisfied with
ceo_commit_digest_steeringis extra, not missing..user.login == $reviewergates the selector, andceo_digest_threadfilters to reviewer+bot before rendering, withhasntassertions on both the login and the injected text.dparsedempty →continue(watermark unmoved, retried) path are correct for idempotence, modulo theBOT_USERissue above.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.🔧 Rework — round 2 (automated)
Addressed the review on
claude-opus-5at effort xhigh — 1 new commit(s).🤖 Review —
COMMENT(automated)CI for
1938714e: successAdds a same-tick "Path 1c" so the CEO acts on reviewer comments left on the open weekly digest instead of waiting for next week's composition. Code is careful, well-commented, and unusually well-tested; CI is green and the checklist matches the diff. I'm not blocking, but there are a few things I want a human to weigh before merge.
Checklist / scope
ceo_digest_pending_steering_number,ceo_digest_thread,ceo_build_digest_steering_prompt,_ceo_parse_work,ceo_parse_digest_steering,ceo_file_digest_workall exist inlib/ceo.sh; Path 1c is wired inbin/tick.sh; the reviewer-author gate is applied in both the selector andceo_digest_thread; the claimed tests (non-reviewer rejected at both layers, ISO-week stamp doesn't suppress, already-replied watermark) are all present. No fabrications found.Things a human should decide on
ceo_file_digest_work, lib/ceo.sh). This is the substantive change: model output derived from a free-text reviewer comment now becomes anAgent-labeled, unassigned ticket that the autonomous grind picks up with no second greenlight. The gate is.user.login == $FORGEJO_REVIEWER(correct, and fails closed on empty$reviewerand empty$BOT_USER), and the code-check gate still runs — but I can't see issue #433's text, so I can't verify the claimed "requirement 6: no second greenlight round-trip" is actually what was asked for. If it isn't, this is a policy change, not a bug fix.continueon an unparseable steering response (bin/tick.sh, Path 1c). Two effects worth confirming are intended: (1) it skips Path 2 for that repo, so a digest comment the model can never parse indefinitely blocks that repo's weekly digest composition, not just the steering reply; (2) unlike the success path (return 0, "one model-backed action per tick"), this burns two model calls and then moves on to the next repo, where more calls can happen in the same tick.Smaller findings
[[ "$last_r" > "$last_b" ]]andsort | lastinceo_digest_pending_steering_number). Fine for same-server RFC3339 strings, but if Forgejo returns a non-Zoffset, a DST transition can misorder the watermark for a one-hour window. Fixtures only coverZ. Low severity; worth a comment or a normalized compare if you care.forgejo_add_label "$repo" "$num" "Agent"is only exercised against a test stub (bin/test-ceo.sh,_AL_ORIGblock) that hardcodes the$2/$3positions. If the real signature differs, the stub masks it — worth an eyeball against another caller inlib/ceo.sh._fjis redefined at the start of the new Phase 5 test block and never restored, so it shadows for the remainder ofbin/test-ceo.sh. The laterceo_codecheck_proposal/forgejo_add_label/logstubs are carefully restored (good), which makes_fjthe odd one out. CI green suggests nothing downstream depends on the old one, but it's a latent trap for the next test added below it.[ -n "$reviewer" ] || return 0early-out (emptyFORGEJO_REVIEWER), though the parallelBOT_USERfail-closed case is covered.Nothing blocking found
No injection/secret/auth issues beyond the authority question above; no CI-workflow changes; reply-before-file ordering and the
</dev/nullon the model-backed gate are both correct and explicitly tested.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.