Skip to content

feat(cogos-harness): threads registry + warn-tier hook (v0.3.0) - #23

Open
chazmaniandinkle wants to merge 14 commits into
mainfrom
feat/threads-registry-v0.3.0
Open

feat(cogos-harness): threads registry + warn-tier hook (v0.3.0)#23
chazmaniandinkle wants to merge 14 commits into
mainfrom
feat/threads-registry-v0.3.0

Conversation

@chazmaniandinkle

@chazmaniandinkle chazmaniandinkle commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a threads registry (bin/threads add|list|check|close, state at ~/.cog/status/threads.json) so a seat's stated open-ended wait ("I'll hold until the verdict lands") survives compaction and session end instead of dying silently in conversation context — the failure this fixes happened on 2026-08-07 (a session watched a CI run and reported "done" when the process exited, before the actual review verdict had propagated).
  • A thread is a resolution predicate, not a completion signal: a bounded shell command whose exit code answers "is the condition true", checkable by anyone at any time with no memory of who registered it. Declared fields only (id/what/why/predicate/opened_at/expected_by/owner/closed_at/closed_reason) — resolved/orphaned/overdue/age are always derived by re-running the predicate, never stored or hand-set.
  • hooks/threads-warn.py (UserPromptSubmit): silent-by-default, per-turn check. It only actually runs a thread's predicate once that thread is past its expected_by (skip_predicate_if_not_due) — a thread with a distant deadline costs nothing turn after turn until it's actually due. Speaks for orphaned threads (unresolved AND past expected_by), and, distinctly, whenever its overall wall-clock budget (COGOS_THREADS_TOTAL_BUDGET) runs out before every open thread could be checked — that case renders an explicit "N not checked this turn — over budget" note rather than staying silent, with scan order rotated across invocations so the same thread never starves the budget check forever. Fail-open on any missing/corrupt state.
  • hooks/threads-gate-pr.py (PreToolUse/Bash): scaffolded but disabled by default — can require an open thread before gh pr create runs, armed only via enforce_pr_create_thread: true in ~/.cog/status/threads-config.json. Nothing in this PR sets that key. Command matching tokenizes the shell command line (splitting on ;/&&/||/|/$(/newline, respecting quotes) and checks whether any segment's argv starts with gh pr create, rather than a bare substring match — so the phrase appearing inside a quoted git commit -m/grep argument doesn't trip the gate. Documented, in-code limit: shell variable-substitution evasion (C=create; gh pr $C) is out of scope for a warn-tier gate that must never itself run a shell.
  • skills/threads/SKILL.md: the procedure, including a worked good-vs-bad predicate example, the skip_predicate_if_not_due behavior, and the budget-notice behavior above — and a real footgun caught empirically while building this (gh ... --jq '.x == "y"' prints false but still exits 0; predicates need [ "$(...)" = "..." ] to be unambiguous).
  • plugin.json + marketplace.json bumped to 0.3.0 (cascaded together, both carried duplicate version/description/keywords for this plugin).

Follow-up commit (f054faa): four blocking hazards closed

An adversarial review of the initial design found four issues, all fixed in a follow-up commit before this PR was reviewed further:

  1. Unbounded predicate output blew the per-turn budget — a predicate like cat /dev/zero cost 10.7s / 7.68GB against a hook with a 4s budget. Fixed by discarding predicate stdin/stdout/stderr (DEVNULL) instead of capturing output nothing reads.
  2. Concurrent threads add silently lost registrations — an unguarded read-modify-write dropped entries under concurrent CLI invocations. Fixed with lib.locked_state(), an flock-guarded context manager used by add/close.
  3. Empty/missing predicate self-reported RESOLVED/bin/sh -c '' exits 0. threads add now rejects a blank --predicate; derive_status() additionally treats a missing/blank predicate as explicit-unresolved as defense in depth for hand-edited state.
  4. The hook ran the predicate before checking whether it could matterorphaned requires overdue, so derive_status always ran the predicate first even for a not-yet-due thread, discarding the result every time. Added skip_predicate_if_not_due (warn-hook only, never threads check).

First review pass: eight findings from an independent blind review, all fixed

A second independent review (2026-08-07) returned REQUEST_CHANGES with eight findings — F1 through F8. All eight are addressed on this branch:

  • F1 (HIGH) — budget exhaustion could silently suppress the entire orphan report when a genuine orphan sat behind enough non-orphaned-but-slow predicates in registry order to exhaust the wall-clock budget (reproduced 5/5 in review). Fixed two ways: the budget note now renders unconditionally, even when zero orphans were found among the threads actually checked; and scan order rotates across invocations (a small persisted counter next to the state file) so the same thread doesn't starve forever. Covered by a deterministic, clock-driven reproduction of the exact starvation scenario, plus a test confirming rotation eventually surfaces the orphan.
  • F2 — the PR-create gate matched a bare substring of the whole command, so git commit -m "docs: ... gh pr create ..." and grep -rn "gh pr create" . both tripped a false deny. Replaced with a tokenizer that splits the command line on shell separators (respecting quotes) and checks whether any segment's argv actually starts with gh pr create. Variable-substitution evasion is explicitly out of scope and documented as such — a warn-tier gate must never run a shell to catch it.
  • F3 — this PR body itself: the original description understated the diff (stale test count, no mention of the f054faa hazard fixes or the skip-if-not-due behavior). Corrected in this same body.
  • F4skills/threads/SKILL.md and README.md corrected to describe actual behavior: predicates only run for due/overdue threads, and the budget-notice behavior from F1 is documented.
  • F5 — gate tests now set COGOS_THREADS_CONFIG to a fixture path unconditionally, so they never resolve to the operator's real ~/.cog/status/threads-config.json.
  • F6 — an unparseable opened_at paired with a duration expected_by used to recompute now + duration fresh every call, a deadline that recedes forever and can never be reached. Same defense-in-depth class as the blank-predicate case: treated as already overdue-eligible instead.
  • F7SCHEMA_VERSION is now enforced on load: a state file with a newer version than this code supports fails loudly (CorruptStateError) rather than parsing silently under the wrong assumptions.
  • F8 — predicate timeout now kills the whole process group (start_new_session=True + os.killpg), not just the immediate /bin/sh child — a predicate that backgrounds or forks something the shell doesn't wait on no longer leaks a reparented process per turn per overdue slow/hanging thread.

Delta pass: independent re-review returned APPROVE, four new residuals closed

The same independent instance re-verified the fixes above with its own reproductions and returned APPROVE (all eight findings CONFIRMED fixed, F1's starvation scenario 5/5 → 0/5 silent, F2 adversarially swept, F8's leak reproduced dead on both trees). It surfaced four new LOW/INFO residuals in the new code, plus a lingering type-confusion gap in F7's fix — all closed in this same pass:

  • NEW-1 — the F2 tokenizer regressed one case the OLD substring match caught for free: GH_TOKEN=x gh pr create (a leading env-assignment token shifts argv, so argv[:3] == ["gh","pr","create"] no longer matched). Fixed by stripping leading NAME=value tokens and a fused subshell ( before the compare; also added single & to the separator set (gh pr create & sleep 1 was previously one un-splittable segment). Backtick substitution and a backslash-escaped quote inside single quotes remain documented, accepted limits. Second-pass residual (NEW-5): the mirror case, a trailing ) fused to create with no flags after it ((gh pr create), FOO=$(gh pr create), (GH_TOKEN=x gh pr create)), also defeated the match — fixed with a per-token rstrip(")") on the first three tokens before the compare.
  • NEW-2 — an F6 thread (unparseable opened_at + duration expected_by) rendered self-contradicting evidence on the triage surface: (overdue) next to age 0s, expected_by <tomorrow>, both synthesized from now standing in for the real timestamp. derive_status() now flags age_unknown/expected_by_unknown and adds an explicit unparseable_opened_at reason; both the warn hook's rendered line and threads check's output now show age ? / expected_by ? instead of the synthesized values.
  • NEW-3 — documented the .threads.json.lock (flock-guarded CLI write lock) and .threads.json.rotation (F1's scan-order counter) sidecar files in the README's env/file reference; previously undocumented.
  • NEW-4 — no code change needed; added a one-line comment at the process-group reap site (proc.wait(timeout=2) after killpg) noting the up-to-2s wall-clock overshoot it can add and that shipped defaults keep the warn hook's worst case under hooks.json's 8s timeout for it.
  • F7 residual — the version guard was isinstance(version, int), which lets a non-int version (a hand-edited "2", a float, true) slide through unchecked, since the newer-than-supported comparison simply never fires for it. Now rejected the same way as a too-new version: CorruptStateError for any present-but-non-int version (bool explicitly excluded from counting as int, despite being a Python int subclass).

Test plan

  • python3 tests/test_threads.py -v — 109/109 passing (37 at initial PR open → 48 after the four-hazard follow-up → 82 after the first review's eight-finding fixes → 102 after the delta pass's four-residual + F7-residual fixes → 109 after the second delta pass's NEW-5 trailing-paren fix)
  • Covers, in addition to the original suite: F1's budget-notice-and-rotation fix (deterministic clock-driven scenarios), F2's command tokenizer (false-positive/negative cases via subprocess and direct unit tests, now including NEW-1's env-assignment/&/subshell-paren cases with DENY-expected armed-gate assertions), F5's hermetic gate-test config, F6's unparseable-opened_at defense-in-depth plus NEW-2's age_unknown/expected_by_unknown rendering (both the warn hook line and threads check's output), F7's schema-version enforcement plus its non-int-version residual, F8's process-group-kill (spawns a real backgrounded descendant, confirms via pgrep that it doesn't survive the timeout)
  • Manually verified hook output for the three required scenarios (nothing registered / one healthy thread / one orphan) — see the SKILL.md worked example
  • Confirmed hooks.json and both .claude-plugin/marketplace.json manifests still parse as valid JSON
  • Not installed or activated anywhere — this PR only ships code; no settings.json/settings.local.json touched

Adds a small threads registry (bin/threads add|list|check|close,
~/.cog/status/threads.json) and its UserPromptSubmit warn hook, so a
seat's stated open-ended wait ("I'll hold until the verdict lands")
survives compaction and session end instead of dying silently in
conversation context.

A thread is a resolution predicate, not a completion signal: a bounded
shell command whose exit code answers "is the condition true", not
"did the process finish" -- the distinction a 2026-08-07 incident
proved matters (watching a CI run reported "done" before the actual
review verdict had landed). Declared fields are the only ones a caller
ever writes (id/what/why/predicate/opened_at/expected_by/owner/
closed_at/closed_reason); resolved/orphaned/overdue/age are always
derived by re-running the predicate, never stored or hand-set.

threads-warn.py is silent by default and only speaks for orphaned
(unresolved + past expected_by) threads, bounded by a hard per-
predicate timeout plus an overall wall-clock budget, fail-open on any
missing/corrupt state per the plugin's per-turn-hook contract.

Also ships, disabled by default: threads-gate-pr.py, a PreToolUse gate
that can require an open thread before `gh pr create` runs (armed via
enforce_pr_create_thread in ~/.cog/status/threads-config.json -- not
set anywhere in this change).

37 self-tests (tests/test_threads.py, stdlib unittest) cover the CLI
round trip, the shared library's state/predicate/derive primitives,
and -- byte-exact against real stdout -- the warn hook's silence
contract for every failure mode: missing/corrupt/empty state file, a
timing-out predicate, an erroring predicate, and unresolved-but-not-
overdue threads.
Adversarial review of #23 reproduced four fail-safety hazards on this
per-turn hook and its CLI:

- run_predicate captured stdout/stderr (capture_output=True) but nothing
  ever read them -- an unbounded-output predicate (cat /dev/zero) blew
  10.7s / 7.68GB into the per-turn hook, past both its own timeout and
  hooks.json's declared budget. Now discards all predicate I/O via
  DEVNULL; the same fix incidentally stops a non-UTF-8-but-exit-0
  predicate from being misreported as unresolved by a decode error, and
  stops predicate output from ever leaking onto the hook's JSON stdout.
  Reproduced fixed: cat /dev/zero predicate now finishes in ~3s (budget-
  bounded) at ~17MB RSS instead of 10.7s / 7.68GB.

- `threads add`/`close` did an unguarded read-modify-write
  (load -> mutate -> atomic_write); atomic_write's tempfile+os.replace
  only makes the final rename atomic, not the read-modify-write around
  it, so concurrent invocations silently clobbered each other while each
  printed "registered" and exited 0. Added lib.locked_state(), an flock-
  guarded context manager around the whole load/mutate/write span; add
  and close now use it. Reproduced fixed: 8 concurrent `add`s that
  previously dropped to as few as 3 survivors now consistently register
  all 8.

- An empty or missing `predicate` made a thread self-report RESOLVED
  (`/bin/sh -c ''` exits 0) -- exactly the "all-clear from nothing"
  failure this registry's derived-not-declared design exists to
  prevent. `threads add` now rejects a blank/whitespace `--predicate`
  outright; derive_status() additionally treats a missing/blank
  predicate as an explicit unresolved result (defense in depth against
  a hand-edited or partially-written state file), never as resolved.

- derive_status() always ran the predicate before checking `overdue`,
  even though `orphaned` requires overdue and the per-turn warn hook
  only ever speaks for orphaned threads -- so a thread with a
  multi-hour/day expected_by paid full predicate latency (and, for the
  documented gh-based predicates, a network round trip) every single
  turn for its entire lifetime, for a result that was always discarded.
  Added `skip_predicate_if_not_due` (used only by the warn hook, never
  by `threads check`, which always wants a real predicate run): skips
  invoking the predicate when the thread isn't past its deadline, since
  the exit code can't change the answer in that case. Bounds: a thread
  becomes exactly as checkable as before the instant it goes overdue
  (skip only applies while not-yet-due); `threads check` is unaffected
  at either bound, since it never sets the flag.

14 new tests cover all four fixes directly (bounded-output timing,
concurrent-add survival, empty/missing-predicate rejection at both the
CLI and library layers, and skip-vs-still-runs-when-overdue for the
per-turn hook), plus the existing 34 all still pass unmodified: 48/48.

Also corrects derive_status()'s docstring, which previously declined to
justify skipping owner-liveness by network-call cost while shelling out
to arbitrary (possibly network-calling) predicate shell in the very next
statement -- now states plainly that predicates may make network calls
and that skip_predicate_if_not_due bounds how often, not whether.
@chazmaniandinkle

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit (f054faa) closing 4 blocking hazards an adversarial review found in the original design:

  1. Unbounded predicate output blew the per-turn budgetrun_predicate captured stdout/stderr but nothing read them; a predicate like cat /dev/zero cost 10.7s / 7.68GB on a hook with a 4s budget and an 8s hooks.json timeout. Fixed with DEVNULL on stdin/stdout/stderr. Reproduced fixed: same predicate now finishes in ~3s (budget-bounded) at ~17MB RSS.
  2. Concurrent threads add silently lost registrations — unguarded read-modify-write; atomic_write's tempfile+replace only makes the final rename atomic, not the surrounding read-modify-write. Added lib.locked_state(), an flock-guarded context manager, used by add/close. Reproduced fixed: 8 concurrent adds (previously dropped to as few as 3) now consistently register all 8.
  3. Empty/missing predicate self-reported RESOLVED/bin/sh -c '' exits 0. threads add now rejects a blank --predicate; derive_status() additionally treats a missing/blank predicate as explicit-unresolved as defense in depth for hand-edited state.
  4. The hook ran the predicate before checking whether it could matterorphaned requires overdue, but derive_status always ran the predicate first, so an open thread with a multi-day expected_by paid full predicate latency (and, for the documented gh-based predicates, a network round trip) every turn of every session, discarded every time. Added skip_predicate_if_not_due (warn-hook only, never threads check): skips invoking the predicate while the thread isn't yet past its deadline, since the exit code can't change the answer in that case. A thread becomes exactly as checkable as before the instant it goes overdue.

14 new tests cover all four directly; existing 34 pass unmodified. Full suite: 48/48 (python3 tests/test_threads.py -v).

@chazmaniandinkle

Copy link
Copy Markdown
Contributor Author

🕶 Independent blind review — REQUEST_CHANGES

Reviewed by a dispatched independent instance (Opus) with review-only authority; artifacts only, no author commentary in scope. Posted by the seat verbatim.

Basis: the warn hook can silently drop a genuine orphan forever when its wall-clock budget is consumed by non-orphaned threads — reproduced 5/5 — which is the exact "wait dies silently" failure class this PR exists to eliminate, and it is the one major branch in the hook with zero test coverage.

F1 — HIGH — Budget exhaustion silently suppresses the entire orphan report — CONFIRMED

hooks/threads-warn.py:128-129 with :104-106, :136-137. The skipped_for_budget note only renders inside the non-empty orphan_lines path. With budget consumed by slow non-orphaned predicates, remaining threads drop at the loop-top check and the hook emits nothing. Reproduced: 3 overdue threads (two slow-resolving, one truly orphaned), COGOS_THREADS_TOTAL_BUDGET=1.0 → byte-empty stdout, 5/5 runs. Scan order is plain registry order, so the same thread starves every turn, deterministically. Fixes (either sufficient): render the over-budget note unconditionally; rotate scan order.

F2 — MEDIUM — PR gate matches a bare substring of the whole command — CONFIRMED

hooks/threads-gate-pr.py:39,69. Armed with zero open threads: git commit -m "docs: describe gh pr create gating"deny (false positive); grep -rn "gh pr create" ./docsdeny; C=create; gh pr $C --title xallow (false negative). The plugin's own SKILL.md/README contain the literal string, so editing this feature's docs trips the gate. Also: "≥1 open thread exists anywhere" satisfies the check — any stale unrelated thread passes it.

F3 — MEDIUM — PR description understates and misreports the diff — CONFIRMED

Test plan says 37/37; branch head has 48/48 (f054faa added 14). Summary never mentions the four blocking-hazard fixes or that predicates are not run at all for not-yet-due threads — a material behavioral property.

F4 — MEDIUM — Shipped docs contradict shipped behavior — CONFIRMED

skills/threads/SKILL.md:141-142,155-156, README.md:38 still describe every-turn predicate checks and omit skip_predicate_if_not_due. SKILL.md is the agent-facing surface with a MUST-level invocation block; an agent reading it will believe its threads are actively re-checked every turn when they are not.

F5 — LOW — Gate tests not hermetic w.r.t. config — CONFIRMED

tests/test_threads.py:540-543: COGOS_THREADS_CONFIG inherited unset → resolves to the operator's real ~/.cog/status/threads-config.json. Arm the flag for real and the "disabled by default" test fails against the live machine.

F6 — LOW — Unparseable opened_at + duration expected_by → deadline recedes forever — PLAUSIBLE

lib/threads_core.py:371-372: opened = parse_ts(...) or now recomputes now + 1d every call; thread permanently invisible to the warn hook. Loads cleanly (load_state validates only truthy id). Same hazard class as the blank-predicate case that did get defense-in-depth.

F7 — LOW — SCHEMA_VERSION written, never read — PLAUSIBLE

threads_core.py:66 vs :177-184. A future v2 state file parses silently under v1 code.

F8 — LOW — Predicate timeout kills the shell, not descendants — PLAUSIBLE

threads_core.py:306-315: subprocess.run(timeout=) kills /bin/sh only; a gh/curl grandchild survives detached. Process leak, one per turn per overdue slow-network thread.

Checked and cleared

bin/threads PATH wiring (CC adds plugin bin/ to PATH; mode 100755) · concurrency (flock + atomic replace; 8-way test passes; lock-free hook reads safe) · fail-open posture verified empirically (missing/corrupt/empty state → empty stdout, exit 0) · _fail() inside locked_state releases cleanly · 48/48 tests pass locally.

Test adequacy

Strong where it aims (byte-exact assertions against real hook stdout). The gap is precisely F1: no test exercises COGOS_THREADS_TOTAL_BUDGET exhaustion at all.

…y as overdue-eligible

F6 in the 2026-08-07 independent review: an unparseable opened_at falls
back to now, which makes a duration expected_by ("1d") recompute to
"now + 1d" fresh on every derive_status() call -- a deadline that
recedes forever and can never be reached, leaving the thread invisible
to the warn hook for its whole life. Same defense-in-depth class as the
existing blank-predicate handling: treat the thread as already
overdue-eligible instead of trusting the receding deadline.
F7 in the 2026-08-07 independent review: SCHEMA_VERSION was written on
every save but never read back on load, so a future schema bump would
parse silently under old-schema assumptions instead of failing loudly.
This is a single-user machine, not a service with compatibility
obligations to a fleet of other readers -- a loud, immediate refusal to
load a newer-than-supported state file is the correct failure mode here.
F8 in the 2026-08-07 independent review: subprocess.run(timeout=...)
only signals the direct /bin/sh child. A predicate that backgrounds or
forks something the shell doesn't wait on (a stray `&`, a curl/gh call
left running) survived its parent's death, reparented and still
running -- one leaked process per turn per overdue slow/hanging thread.

run_predicate now runs /bin/sh in its own session
(start_new_session=True) and kills the whole process group via
os.killpg on timeout, reaching every descendant in one signal.
… scan order

F1 (HIGH) in the 2026-08-07 independent review: budget exhaustion could
silently suppress the entire orphan report when a genuine orphan sat
behind enough non-orphaned-but-slow predicates in registry order to
exhaust the wall-clock budget -- reproduced 5/5 in review, because scan
order was plain registry order every turn, so the same thread starved
identically every time.

Two fixes:
- The budget note now renders unconditionally (`_render_block`), even
  when zero orphans were found among the threads actually checked --
  silence previously meant either "all healthy" or "ran out of time,"
  indistinguishably.
- Scan order rotates across invocations (`_rotation_offset`, a small
  persisted counter next to the state file) so a thread skipped for
  budget this turn is checked earlier on a later one instead of
  starving forever.

Covered by a deterministic, clock-driven reproduction of the exact
starvation scenario (no reliance on real predicate/OS timing, which is
inherently a race) plus a test confirming rotation eventually surfaces
the orphan.

Also corrects skills/threads/SKILL.md and README.md (F4), which still
described the warn hook as checking every open thread's predicate every
turn and didn't mention the budget-notice behavior above.
…eal config

F5 in the 2026-08-07 independent review: TestGatePrScaffoldDisabledByDefault
only set COGOS_THREADS_STATE per test, not COGOS_THREADS_CONFIG. Any test
that doesn't explicitly arm the gate with its own config fixture inherits
the unset env var, which resolves to the operator's real
~/.cog/status/threads-config.json -- if that file happens to have
enforce_pr_create_thread=true set on the machine running the tests, the
"disabled by default" assertions fail against live state instead of a
fixture. Every test in the class now gets its own nonexistent-by-default
config fixture path via setUp.
…re substring

F2 in the 2026-08-07 independent review: the PR-create gate matched a
bare substring of the whole command, so `git commit -m "docs: ... gh pr
create ..."` and `grep -rn "gh pr create" .` both tripped a false deny
-- editing this feature's own docs would trip the gate. Also: any stale
unrelated open thread satisfied the "at least one open thread" check.

Replaced with a tokenizer (_split_commands / _looks_like_gh_pr_create)
that splits the command line on shell separators (; && || | $( newline),
respecting quotes, and checks whether any segment's argv actually starts
with `gh pr create` rather than merely containing the words somewhere.

Variable-substitution evasion (`C=create; gh pr $C`) is explicitly out
of scope and documented as such in the tokenizer's docstring -- a
warn-tier gate must never itself run a shell to catch that, and this
gate's only job is to catch a literal invocation.

Updates the README test count and adds a summary of all fixes from this
review pass, now that the full suite (82 tests) is in place.
@chazmaniandinkle

Copy link
Copy Markdown
Contributor Author

🕶 Independent blind review — delta pass — APPROVE (head 74a7728)

Same independent instance as the first review, re-verifying with its own reproductions. Posted by the seat verbatim (condensed; full report held by the seat).

Basis: all eight findings substantively fixed — F1's starvation goes from 5/5 silent to 0/5 silent with both the unconditional budget notice and scan rotation independently verified; F2's false positives gone with every true positive preserved; F8's process leak reproduced dead — 82/82 tests, no regression to the silence contract, residuals fail-open or cosmetic.

finding verdict evidence
F1 (HIGH) FIXED — CONFIRMED Identical repro, 5 turns: before = 5×[SILENT]; after = budget notice turn 1, orphan surfaced turns 2/3/5 via rotation. TestWarnHookBudgetFairness drives the exact scenario with an injectable clock.
F2 FIXED — CONFIRMED Full table re-run: both false positives now ALLOW, all seven true positives still DENY. Tokenizer survived adversarial sweep (unbalanced quotes, nested $( ), 200 KB command, 50k ;) — no raise, no hang, ≤0.57 s.
F3 FIXED — CONFIRMED Body's 82/82 matches measured; f054faa hazards disclosed; skip-if-not-due described. Nit: "eight findings, all fixed" section enumerates seven — F3 itself absent (self-evidencing).
F4 FIXED — CONFIRMED All three doc sites checked against actual behavior.
F5 FIXED — CONFIRMED Gate tests hermetic via fixture COGOS_THREADS_CONFIG.
F6 FIXED (detection) — CONFIRMED Correct overdue-eligibility; see NEW-2 for the rendering side effect.
F7 FIXED — CONFIRMED version: 99 → warn hook silent (fail-open surface), threads list loud, exit 1. Residual: isinstance(int) guard lets string "2" load.
F8 FIXED — CONFIRMED Same probe both trees: OLD leaked the backgrounded descendant, NEW did not. killpg-on-zombie race safe by construction.

Regression check: silence contract re-verified directly (missing/empty/corrupt state, no threads, future schema, healthy not-yet-due → all SILENT rc=0). Rotation counter survives every hostile state constructed (non-integer, negative, directory, unwritable parent) → offset-0 fallback, all orphans still reported.

New findings (all LOW/INFO, fail-open)

  • NEW-1 — residual gate false negatives: GH_TOKEN=x gh pr create (leading env assignment — the old regex caught this; ordinary usage, not evasion), single & not a separator, (subshell) paren fusing, backticks, backslash-in-single-quotes. Fix: strip leading NAME=value tokens; add & to separators.
  • NEW-2 — F6 threads render self-contradicting evidence: (overdue) beside age 0s, expected_by <tomorrow> because opened falls back to now. Suggest an explicit reason token and age ? rendering.
  • NEW-3 — INFO: sidecar files .threads.json.rotation / .threads.json.lock in ~/.cog/status/ absent from the README file reference.
  • NEW-4 — PLAUSIBLE: killpg reap (proc.wait(timeout=2)) can overshoot the wall-clock budget by 2 s per timing-out predicate; safe at shipped defaults (~5.2 s vs 8 s hooks.json timeout), reachable only by env misconfiguration.

Delta-pass residual on F7 (2026-08-07 independent review): the version
guard was `isinstance(version, int) and version > SCHEMA_VERSION`, which
lets a non-int version (a hand-edited "2", a float, `true`) slide through
un-checked -- isinstance(int) is simply False for it, so the
newer-than-supported branch never fires. Now any present-but-non-int
version raises CorruptStateError the same way a too-new one does. bool
is explicitly excluded despite being a Python int subclass -- "version":
true is not a version number.
…ound

NEW-4 (2026-08-07 independent review, delta pass): plausible-but-cosmetic
residual, no behavior change. The proc.wait(timeout=2) reap after
killpg on a timed-out predicate can itself add up to 2s of wall-clock
overshoot. One-line comment records that it's bounded and that shipped
defaults (THREADS_PREDICATE_TIMEOUT 3s, THREADS_TOTAL_BUDGET 4s) keep
the warn hook's worst case well under hooks.json's 8s timeout for it.
…l evidence

NEW-2 (2026-08-07 independent review, delta pass): an F6 thread
(unparseable opened_at + duration expected_by) rendered self-
contradicting evidence on the triage surface -- "(overdue)" right next
to "age 0s, expected_by <tomorrow>", both synthesized from `now`
standing in for the corrupt real timestamp.

derive_status() now flags ThreadStatus.age_unknown (set whenever
opened_at didn't parse, regardless of expected_by's shape) and
expected_by_unknown (set only when expected_by_ts was itself derived
from the guessed opened_at), and adds an explicit "unparseable_opened_at"
reason. Both the warn hook's rendered line and `threads check`'s output
now show "age ?" / "expected_by ?" instead of the synthesized values.
`threads list` already computed age independently and needed no change.
…sidecars

NEW-3 (2026-08-07 independent review, delta pass): .threads.json.lock
(locked_state()'s flock-guarded CLI write lock) and .threads.json.rotation
(the warn hook's F1 scan-order counter) were undocumented in the README's
env/file reference. Both are named from COGOS_THREADS_STATE, not
independently configurable, and are safe to delete by hand -- every read
failure on either degrades to a default rather than an error.
… in PR-create gate

NEW-1 (2026-08-07 independent review, delta pass): the F2 tokenizer
regressed one case the OLD bare-substring match caught for free --
`GH_TOKEN=x gh pr create` -- because a leading env-assignment token
shifts argv, so `argv[:3] == ["gh","pr","create"]` no longer matched.
Ordinary usage, not evasion.

Fixed by stripping leading `NAME=value` tokens and a subshell `(` fused
to the first token before the argv compare (_strip_leading_noise()).
Also added a single `&` to the separator set -- `gh pr create & sleep 1`
was previously one un-splittable segment.

Backtick command substitution and a backslash-escaped quote inside a
single-quoted string remain documented, accepted limits (added to the
in-code limits comment alongside the existing variable-substitution-
evasion one).
NEW-5 (2026-08-07 independent review, second delta pass): a trailing
`)` fused to the last of the first three tokens defeated the subshell
strip in the no-flags case -- `(gh pr create)`, `FOO=$(gh pr create)`,
and `(GH_TOKEN=x gh pr create)` all tokenized their final token as
`create)`, which _strip_leading_noise() (opening-paren only) never
touched, so all three ALLOWed instead of DENYing.

Fixed with a per-token rstrip(")") on the first three tokens right
before the argv[:3] == ["gh","pr","create"] compare in
_looks_like_gh_pr_create(), alongside the existing leading-noise strip.
Documented in both the function's own docstring and the closing-paren
note added to _split_commands()'s limits list.
@chazmaniandinkle

Copy link
Copy Markdown
Contributor Author

🕶 Independent blind review — final pass — APPROVE (head 2bdbfb2, NEW-5 closed at 929cbbe)

Same independent instance, third pass. Posted by the seat verbatim (condensed).

Basis: all five residuals substantively fixed and verified by reproduction — every NEW-1 case now denies while all F2 false positives stay allowed; NEW-2 renders age ? / expected_by ? with an explicit unparseable_opened_at reason on all three surfaces; F7 rejects every non-int version — 102/102 passing at review head, silence contract intact.

  • NEW-1 FIXED — full table re-run: GH_TOKEN=x …, A=1 B=2 …, single &, subshell parens all DENY; documented limits (var-substitution, backticks, backslash-in-single-quotes) correctly remain and are now enumerated in the docstring. No F2 regression in either direction; &&/|| precedence over &/| verified correct.
  • NEW-2 FIXED — before/after render confirmed on warn hook, threads check, threads list; the narrower flag is right: unparseable opened_at + absolute expected_by keeps the real deadline (a measurement, not a guess) and no false reason.
  • NEW-3 FIXED — sidecars documented incl. delete-safety and read-failure degradation.
  • NEW-4 FIXED as specified — and the comment's arithmetic verified empirically: worst-case 4-thread hang+background scenario measured 4.24s against the 8s hooks.json ceiling.
  • F7 residual FIXED"2", 2.0, true, [1], {} all rejected loud via CLI / silent via hook; bool explicitly excluded despite subclassing int.

One further LOW found and since closed (929cbbe, 109/109): trailing ) fused to create defeated the subshell strip — (gh pr create) allowed. Prescribed one-line per-token paren strip applied with DENY tests for the three exact cases; seat spot-verified post-push (three MATCH, false-positive case still clean).

Arc: initial blind review (REQUEST_CHANGES, F1–F8) → 8 fixes → delta re-review with independent reproductions (APPROVE + 4 residuals) → residual fixes → targeted final verify (APPROVE + NEW-5) → NEW-5 closed. Tests 37 → 48 → 82 → 102 → 109.

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