Skip to content

task-work silently writes no TAB_ID when the tab lookup misses: report and log the miss at launch (#242) - #243

Merged
martin-conur merged 3 commits into
mainfrom
task/tabid-capture-silent
Oct 2, 2026
Merged

martin-conur merged 3 commits into
mainfrom
task/tabid-capture-silent

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Closes #242

What changed

Part 1: the miss is loud, logged, and never fatal. aw_record_tab_id in lib/zellij-tab.sh replaces the silent capture block in:

  • all seven task-work copies (the drift-guarded info-tab-id region)
  • both task-reviewer copies and task-recreate-worker, which had the same three lines

When the lookup misses, it prints on stderr why, names the slug it searched for, and appends a line to ~/.task-force/radio/log (tab-id: <caller> capture missed slug=… reason=… info=… detail=…). The reasons it tells apart:

  • no-zellij-bin: zellij is not on PATH
  • not-in-zellij: $ZELLIJ is unset
  • no-jq: jq is not on PATH
  • list-tabs-empty: zellij listed nothing
  • race: the tab is listed by the time the diagnosis re-queries
  • no-match: carries the slug's length and every tab name zellij reported, JSON-quoted, so a byte-level mismatch shows in the line itself

A successful capture prints nothing, so output is unchanged (regression test included). A miss still lets the worker launch.

The two ends are linked. task-done's Skipping zellij close-tab (no tab id captured …) line gains a second line naming which source was empty:

  • $ZELLIJ unset
  • no sidecar at the path it derived from the branch
  • a sidecar with no TAB_ID=, with the exact grep 'tab-id:.*slug=<slug> ' …/radio/log that finds the launch-time line

The reason is worked out when the id is read, because the sidecar is deleted before the close step reports it. task-done still never falls back to the unscoped close-tab (#107 / #108).

Part 2: the trigger was not reproduced

I could not reproduce the trigger. The issue's evidence turns out to have a simpler explanation:

  • All nine sidecars without a TAB_ID (gdi-chatbot) have mtimes of 2026-05-12 to 2026-05-14. The capture was added by Fix #117: preserve TAB_ID across re-register; persist TAB_ID in $INFO_FILE #118 on 2026-05-24, so they predate the feature. Every sidecar on this machine written since has its TAB_ID.
  • I checked both suspected shapes against a real zellij 0.44.2 in a detached session (zellij attach --create-background, not the live one). The 50-character bug-model-renders-table-but-the-chat-already-outpu, its 56-character collision form …-hu1s5, and add-readme-7qegu all came back byte-equal from list-tabs --json, matched right after new-tab.

So the intermittent reports come from a condition those artifacts do not record. The next one will name itself in one line. That also covers misses on task-done's side, which are not capture failures at all: a branch switched away from task/<slug> (no sidecar found), or task-done run outside zellij.

Sequencing: #237

This change lives in task-work's info-tab-id region, which #237 (the tracker × agent split from #122 Phase 2) deletes and relocates. #237 must carry this forward into its new modules, not reinvent it. The behaviour lives in lib/zellij-tab.sh (aw_record_tab_id), so in practice the relocated code just needs to call it.

Docs

  • A new "When radio misbehaves" row in the README, the seven steering templates and the dogfood .claude/gh-workflow.md.
  • A runbook test that checks the documented grep string is one lib/zellij-tab.sh actually logs.
  • A CHANGELOG entry. Upgrading needs no task-init re-run: the scripts reach machines through symlinks. A re-run only adds the new runbook row to an installed workflow doc.

Verification

  • ./run_tests.sh: 1393/1393 (baseline 1380 + 13 new). Most are in tests/tab_id_capture.bats; it covers each reason code, a miss with zero exit and a log line, a silent hit, the kiro-gh loadout, and the three task-done reasons. One more is in tests/radio_runbook.bats.
  • tools/check-drift.sh: 35 groups clean.
  • shellcheck -x: clean on every changed shell file.

🤖 Generated with Claude Code

martin-conur and others added 2 commits October 2, 2026 09:30
…t and log the miss at launch, link it from task-done (#242)

A missed tab-id capture used to append nothing and print nothing, so its
only symptom was task-done's "no tab id captured" hours later, in another
command. `aw_record_tab_id` in `lib/zellij-tab.sh` now replaces the silent
block in all seven task-work copies (drift-guarded `info-tab-id` region),
both task-reviewer copies and task-recreate-worker. On a miss it says why on
stderr — not-in-zellij, no-jq, list-tabs-empty, race, or no-match with the
tab names zellij reported — and appends a `tab-id:` line to radio's log. A
hit prints nothing; a miss never aborts the launch.

task-done's skip line gains a second line naming which source was empty,
including the grep that finds the launch-time `tab-id:` line. It still never
falls back to the unscoped close-tab (#107 / #108).

Trigger not reproduced: the nine TAB_ID-less sidecars on this machine all
predate #118, which added the capture; 50-char and collision-suffixed names
match byte-for-byte in zellij 0.44.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n if, not A && B || C, for the log append (SC2015)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur

Copy link
Copy Markdown
Owner Author

Spec compliance

Issue #242 specified two parts:

Part 1 — loud, logged, non-fatal capture. ✅ Fully delivered. aw_record_tab_id in lib/zellij-tab.sh replaces the silent three-line block in all seven task-work copies (drift-guarded info-tab-id region), both task-reviewer copies, and task-recreate-worker. On a miss it prints to stderr with the reason and slug, appends a tab-id: line to radio's log with six structured fields, and still lets the launch succeed. A hit prints nothing (regression-pinned in tests). All six reason codes (no-zellij-bin, not-in-zellij, no-jq, list-tabs-empty, race, no-match) are wired through from aw_zellij_tab_id_miss_reason.

Part 2 — find the trigger. ✅ Honestly assessed. The PR provides a solid analysis: all nine sidecars without TAB_ID predate the feature (mtimes 2026-05-12/14 vs. feature landing 2026-05-24), and both suspected shapes (50-char truncation, collision suffixes) were verified byte-equal against real zellij 0.44.2. The trigger remains unknown, and the PR is transparent about that rather than overclaiming.

task-done linking. ✅ The skip message now adds a second line that distinguishes $ZELLIJ unset, no sidecar, and sidecar-with-no-TAB_ID= (with the exact grep string to find the launch-time log line). The $INFO_FILE is deleted before that step runs — the code correctly pre-computes which source is empty before deletion.

Constraints respected. ✅ No fallback to unscoped close-tab (#107/#108). Drift guard covers all seven info-tab-id region files (check-drift.sh line 32). Docs updated in README, .claude/gh-workflow.md, all five steering templates, and CHANGELOG. Runbook test pins the grep 'tab-id:' string against the function that emits it.

CI. ✅ Green, 1 run for head commit 1cda772 (gh run list -c <sha> returns databaseId: 37008325950, conclusion: success). 1393/1393 tests (baseline 1380 + 13 new).


Code-review findings

Nit 1 — no-zellij-bin undocumented in runbook rows and untested

lib/zellij-tab.sh:61 defines the no-zellij-bin code, the PR description lists all six codes, but every runbook table row added in this PR (README, .claude/gh-workflow.md, five steering templates) lists only five: not-in-zellij, no-jq, list-tabs-empty, race, no-match. A user grepping a log line with reason=no-zellij-bin would find it undocumented. There is also no direct test for this code path — the no-jq test places a zellij stub on PATH, so zellij is always present; removing the stub would exercise no-zellij-bin.

Fix: add no-zellij-bin to the reason-code list in the runbook rows (seven files), and add one test case:

@test "miss reason: no-zellij-bin when zellij is not on PATH" {
  NOJQ_BIN=$(make_nojq_bin)   # temp dir with no binaries
  run env PATH="$NOJQ_BIN" bash -c 'source "$1"; aw_zellij_tab_id_miss_reason "$2"' \
      _ "$REPO_ROOT_REAL/lib/zellij-tab.sh" my-feature
  assert_success
  assert_output --regexp '^no-zellij-bin '
}

Nit 2 — no direct tests for task-reviewer / task-recreate-worker callers

tests/tab_id_capture.bats covers the task-work caller and the kiro-gh loadout. bin/task-reviewer, claude-gh/bin/task-reviewer, kiro-gh/bin/task-reviewer, and bin/task-recreate-worker were each changed from the inline three-line block to aw_record_tab_id, but none has a test that checks the log line names caller=task-reviewer or caller=task-recreate-worker. A future refactor that accidentally passes the wrong caller string would be invisible to the test suite.

Not a blocker — the shared function path is well-covered — but worth adding a brief cross-loadout smoke test similar to the kiro-gh test for at least task-reviewer.

Nit 3 — triple list-tabs --json round-trip on the miss path

aw_record_tab_id calls aw_zellij_tab_id_by_name (one list-tabs call), then aw_zellij_tab_id_miss_reason immediately calls list-tabs again for the names array, then calls aw_zellij_tab_id_by_name a third time for the race-detection re-query. The data from the first call is discarded rather than passed through. Three synchronous zellij IPC round-trips on every miss.

This is the correct trade-off for now — the race re-query genuinely needs to be a fresh call, and the diagnostic function's clean interface matters more than micro-optimising a failure path. Noting it because the no-match detail line includes the full tab-names JSON, so any future reader wondering why it runs list-tabs twice in the miss branch will want this context.


Pre-existing issues observed (out of this PR's scope)

The code-review scan surfaced two classes of pre-existing issue in files not touched by this PR:

  1. Indented heredoc terminators in command prompts. claude-gh/commands/reviewer.md (and worker.md, planner.md, pm.md) show heredoc terminators with leading spaces in their code examples, which contradicts the prose rule this PR's reviewer prompt teaches. A model agent copying the example literally would emit an unterminated heredoc. These files were not changed here; worth a separate issue.

  2. Model-filled slot in a double-quoted --body string. claude-local/commands/planner.md has --body "spec written into tasks/NNN-slug.md, ready to dispatch" where NNN-slug is model-filled — if a task slug ever contained a backtick or $, the shell would silently execute or drop it. Pre-existing, out of scope here.


Verdict

clean-with-nits

The spec is fully satisfied, CI is green, the failure shape is correctly diagnosed and fixed, and the approach (loud-at-launch, logged, non-fatal) is right. The nits above are all fixable in this PR: the no-zellij-bin documentation gap is a one-liner across seven files, the missing test is small, and the triple-call note needs no code change (just a comment or a future follow-up). None blocks merge — PM's call on whether to loop the worker for the doc/test nits before landing.

…ent and test no-zellij-bin, test the reviewer and recreate-worker callers (#242 review)

- `no-zellij-bin` joins the reason list in the runbook row of all nine docs
  that carry it, with a test that drops zellij from `PATH`.
- `task-reviewer` (claude and kiro-gh) and `task-recreate-worker` each get a
  test that a miss is reported and logged under their own caller name; the
  recreate-worker one also pins that the stale TAB_ID is stripped and not
  replaced.
- A comment in `aw_record_tab_id` says why the miss path re-queries
  list-tabs up to three times, so nobody folds the calls together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur
martin-conur merged commit c7451a5 into main Oct 2, 2026
4 checks passed
martin-conur added a commit that referenced this pull request Oct 2, 2026
…dy (#236)

Rebase onto origin/main after #243 (#242) merged first. The two PRs overlap on
nine files -- CHANGELOG.md, README.md and all 7 */bin/task-done -- and this one
DELETES those seven, so a resolution that took either side whole would have
lost a P0's work or this one's. Carried forward rather than resolved by
deletion:

1. #243's _TAB_ID_MISS block and the second skip line, which name WHICH source
   came up empty and print the `grep 'tab-id:'` that finds the launch-time log
   line. Seven copies of that became one when task-done was inverted, so it
   belongs in the canonical body.

2. Both README edits: this PR's module-shape section and #243's runbook row.
   #243's nine runbook docs -- 7 templates, README, and this repo's own
   .claude/gh-workflow.md -- are otherwise untouched; verified all nine.

3. Both CHANGELOG entries: this one under Changed, #243's under Fixed.

4. A fourth item not on the carry-forward list: #243's three task-done cases in
   tests/tab_id_capture.bats ran through $CLAUDE_GH_TASK_DONE, a helper var this
   PR retires. Retargeted to `env AW_IMPL=claude-gh "$TASK_DONE"`, the same
   treatment the other suites got. THEIR ASSERTIONS ARE UNCHANGED AND THEY PASS
   AGAINST THIS SHAPE, which is the evidence the port is faithful rather than
   plausible.

Counts move again, and the previous figures were wrong against the wrong
baseline: origin/main is now 1397 because #243 added 17 tests of its own, not
the 1380 this branch had been quoting. Measured by checking origin/main out
into a throwaway worktree and counting it rather than inferring:

  1397 baseline - 32 retired + 14 added = 1379
  bats --count tests/*.bats             = 1379
  ./run_tests.sh                        exit 0 -- 1379 ok, 0 not ok

The CHANGELOG now names the REF as well as the tool, since "1380" was correct
against the wrong baseline -- a third variant of the count-credibility problem
raised in review, after the grep-vs-bats one.

check-drift 25 groups. shellcheck clean on the full CI glob.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
martin-conur added a commit that referenced this pull request Oct 2, 2026
…tical copies (#236) (#244)

* task-done: one canonical body + tracker modules, deleting 7 near-identical copies (#236)

task-done was a three-variant file wearing seven copies. claude-gh,
claude-notion, kiro-gh and kiro-notion were byte-identical; claude-local and
kiro-local were byte-identical to each other; claude-jira differed by seven
lines. 1525 lines expressing about 249 distinct ones, held in step by ten of
the 35 groups in tools/check-drift.sh.

The part that genuinely differed was the part with no sentinel on it: the
`gh pr view` / `gh pr create` block sat between two drift-guarded regions and
was guarded by neither, so jira's Jira-key uppercasing could have broken in one
copy unnoticed. The local board-regen region was nested inside
confirm-and-cleanup, and extract_region is a plain awk window, so the outer
extract swallowed the inner sentinels and passed only because both local copies
happened to match.

There is no agent axis here at all, and that shaped the design: removing a
worktree, sweeping radio state and closing a tab never depended on which agent
was in the tab. So a loadout is {tracker x agent} and the modules are keyed on
the axes rather than the combo -- lib/trackers/{gh,jira,notion,local}.sh and
lib/agents/{claude,kiro}.sh, four files and two, not seven of each. kiro-jira
(#92) reuses jira.sh x kiro.sh with zero new module files for this tool.

Three hooks carry it and four of the seven loadouts override none:
aw_tracker_pr_section (jira), aw_tracker_usage_steps and
aw_tracker_post_cleanup (local). lib/trackers/_default.sh holds what the four
byte-identical copies actually had, so gh.sh and notion.sh override nothing
rather than restating an identical PR section -- otherwise seven copies would
merely have become four. aw_tracker_module / aw_agent_module live in
lib/detect-impl.sh beside aw_all_impls, and aw_all_trackers / aw_all_agents
derive the axis names from that list rather than restating them.

No behaviour changes. One candidate was designed out rather than accepted:
aw_regenerate_board defaults its `self` to ${BASH_SOURCE[1]}, which from a
tracker module resolves to a file with no task-board beside it, so the sibling
fallback would silently find nothing. aw_tracker_post_cleanup now takes the
impl and names that loadout's own task-done, keeping $PATH-first-then-sibling
resolution byte-identical. The two local board tests caught it on the first run.

Tests ran 1380 -> 1360: 32 duplicated cases retired, 12 added. The port was
done in two steps on purpose -- retarget all 67 task_done cases at the
canonical script and confirm green first, then retire -- so behaviour
preservation is an observation rather than a hope. tests/loadout_modules.bats
proves every module LOADS (derived from aw_all_impls, failing on a missing
file, an orphan file, or a module missing a hook) and tests/task_done.bats
proves the right one RAN. Both were demonstrated to fail, not assumed to.

Upgrading: no task-init re-run needed. All seven installers already symlink
bin/task-done and never their own copy, so no installer-written artifact
changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* task-done: address review on #236 — call-site double error, stale comment, count parity (#244)

Three items from the clean-with-nits review on PR #244.

1. tests/task_done_dispatcher.bats:56 said "kiro-notion's task-done prints
   branch/base lines", true pre-#236 and not after. The comment now says what
   the test pins: detection reached the notion module and the SHARED body
   produced those lines, there being exactly one task-done since #236.

2. The test count disagreed between the two durable records -- PR body
   1380 -> 1360, CHANGELOG 1381 -> 1361. Nets agreed at -20, absolute
   baselines did not, which undercuts the credibility the retirement table
   exists for. Cause found rather than a side picked: `bats --count` and
   `grep -c '^@test'` differ by one because tests/radio_home_isolation.bats:105
   has an @test inside a heredoc that writes a generated probe.bats fixture, so
   grep counts a test bats never runs. The measured count is authoritative;
   the CHANGELOG now states which tool produced it and shows the arithmetic.

3. The composition call site was
       source "$(aw_tracker_module "$AW_ROOT" "$TRACKER")" || exit 1
   which reads as resolve-then-source and is not: on a missing module the
   helper writes its diagnosis and exits 1, but the substitution has already
   yielded the empty string, so `source ""` runs and bash appends
   `: No such file or directory` naming a line in task-done before `|| exit 1`
   is reached -- inviting a reader to debug the wrong file. Now two lines.

   The part worth recording is why the existing test missed it. `aw_tracker_module
   names the missing file rather than letting source fail` passed either way,
   because it exercised the helper directly and never went through the call
   site: a name promising more than it checked, so a green suite would have
   told the next reader something untrue. Same shape as the misnamed table row
   this branch caught in its own test rename. Both helper tests are renamed to
   say they test the helper, and two new cases run the real bin/task-done
   against a checkout with a module removed and refute the shell noise -- one
   for gh, one sweeping all four trackers.

   Both were verified to FAIL against the old call site before landing, which
   the first draft did not: it asserted `source: : not found`, the message the
   review described, rather than the one bash actually emits. That draft passed
   against the very call site it was written to reject.

Counts: 1380 baseline - 32 retired + 14 added = 1362, and bats --count returns
1362. Suite exit 0, 1362 ok, 0 not ok. check-drift 25 groups. shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* task-done: carry #243's skip-line fix and tests into the canonical body (#236)

Rebase onto origin/main after #243 (#242) merged first. The two PRs overlap on
nine files -- CHANGELOG.md, README.md and all 7 */bin/task-done -- and this one
DELETES those seven, so a resolution that took either side whole would have
lost a P0's work or this one's. Carried forward rather than resolved by
deletion:

1. #243's _TAB_ID_MISS block and the second skip line, which name WHICH source
   came up empty and print the `grep 'tab-id:'` that finds the launch-time log
   line. Seven copies of that became one when task-done was inverted, so it
   belongs in the canonical body.

2. Both README edits: this PR's module-shape section and #243's runbook row.
   #243's nine runbook docs -- 7 templates, README, and this repo's own
   .claude/gh-workflow.md -- are otherwise untouched; verified all nine.

3. Both CHANGELOG entries: this one under Changed, #243's under Fixed.

4. A fourth item not on the carry-forward list: #243's three task-done cases in
   tests/tab_id_capture.bats ran through $CLAUDE_GH_TASK_DONE, a helper var this
   PR retires. Retargeted to `env AW_IMPL=claude-gh "$TASK_DONE"`, the same
   treatment the other suites got. THEIR ASSERTIONS ARE UNCHANGED AND THEY PASS
   AGAINST THIS SHAPE, which is the evidence the port is faithful rather than
   plausible.

Counts move again, and the previous figures were wrong against the wrong
baseline: origin/main is now 1397 because #243 added 17 tests of its own, not
the 1380 this branch had been quoting. Measured by checking origin/main out
into a throwaway worktree and counting it rather than inferring:

  1397 baseline - 32 retired + 14 added = 1379
  bats --count tests/*.bats             = 1379
  ./run_tests.sh                        exit 0 -- 1379 ok, 0 not ok

The CHANGELOG now names the REF as well as the tool, since "1380" was correct
against the wrong baseline -- a third variant of the count-credibility problem
raised in review, after the grep-vs-bats one.

check-drift 25 groups. shellcheck clean on the full CI glob.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@martin-conur
martin-conur deleted the task/tabid-capture-silent branch October 2, 2026 23:15
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.

task-work silently writes no TAB_ID when the tab lookup misses, so 'no tab id captured' surfaces hours later in task-done

1 participant