Skip to content

PTYCAP-08: periodic task QC (deterministic auditor, flag OFF) [QC BLOCKED] - #513

Draft
finedesignz wants to merge 142 commits into
mainfrom
phase-ptycap-08-task-qc
Draft

finedesignz wants to merge 142 commits into
mainfrom
phase-ptycap-08-task-qc

Conversation

@finedesignz

Copy link
Copy Markdown
Owner

PTYCAP Phase 8 - Periodic Task QC + Optimization

Stacked on #506 -> #505. Merge those first (base of this branch is the Phase 4 branch; this PR targets main so Woodpecker qc runs).

Status: DRAFT - /qc BLOCKED (1 open MAJOR after 2 fix rounds). Feature flag default OFF = true no-op.

What it builds

  • Deterministic hub-side audit engine (hub/src/task-qc/) - NO model turn. Guard tests prove a run produces no user_message, dispatch, session launch, chain_task or token_usage.
  • Six pure detectors against real columns: FAILING, SKIP_FOREVER, NEVER_FIRED, MISCONFIGURED, DEAD_SESSION, OPTIMIZE (__internal_* rows never flagged; log_check skipped is clean).
  • Literal auto-fix allowlist; the writer rejects any unlisted kind (guard test); never disables a task, never mutates in-flight work (writer-side row locks); empty allowlist => report-only. recompute_next_run_at is a proposal, not a fix.
  • System-owned scheduled_tasks row task_type='task_qc' (additive CHECK-list update; boot seed idempotent, before registry/catchup); API refuses POST/DELETE and any repurposing PATCH (incl. converting another task to task_qc).
  • Deterministic bounds (tasks, history rows, fixes, rows written, hard engine deadline via SET LOCAL statement_timeout); Phase-4 lifetime counter key taskqc:<userId>; kill switch inherited.
  • One digest email (E4A from_inbox_id) with one proposal per non-fixed finding; only rendered findings are marked delivered.
  • 08-SPEC TQC-07 and 09-SPEC inherited-budget table rewritten (no token ceiling: deterministic engine).

QC

Seats: sonnet correctness + adversary lenses, Codex read-only (agy MISSING). Record: .planning/phases/PTYCAP-08-task-qc/QC/.
Open MAJOR (QC/VERDICT.md): the sender's whole-run deadline is not hard (deadline starts after recipient query/reservation; markDelivered pool acquisition unbounded).

Verification

  • bun run check-baseline: OK (pass 3165, skip 259, fail 0); floors unchanged.
  • tsc at the pre-phase count.
  • Real-Postgres helpers (schema double-apply, CHECK list, row-lock barrier) first run in Woodpecker.

🤖 Generated with Claude Code

finedesignz and others added 30 commits October 13, 2026 10:28
Goal-backward verification of Phase 1 (PTY token accounting, #395) and
Phase 2 (PTY pre-flight gate, #493) against ROADMAP success criteria, with
every proving test run per-file.

- Phase 1: 3/3 SC implemented; gaps_found (G-1 MAJOR: emitter dedupes on
  record uuid, not message.id, so the PTY ledger over-counts; G-2 MINOR:
  live-Postgres proofs not observed locally).
- Phase 2: passed 3/3; three MINOR gaps (CRLF-fragile guard test, stale
  docs mechanism claim, Telegram permission keystrokes un-preflighted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read-only recon of the gate chain, actor inference, PTY write paths,
counters, owner notification, token-cap defaults and kill-switch
precedents, with file:line evidence. Input to discuss/plan for the
governed-automation guard and the lifetime counter + kill switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ONTEXT

Unattended discuss for Phase 3 (governed-automation guard): the eight
design forks were answered by a claude+codex+agy design-fork panel (record
in GATES/design-fork-1.md, VERDICT PASS with amendments). CONTEXT captures
the binding decisions D-01..D-17: governance primitives only (no PTY
byte-producer), fail-closed lifetime/kill-switch slots, enabled-ceiling
rule, call-time flag default OFF, hard rename with eligible-actor rule,
automation-only governed chain (human chain untouched), door pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Planner (opus) output verified by gsd-plan-checker: 0 blockers, 2 warnings
(fails_when pass-count floors; plan 03-04 doc-heavy scope), accepted for
execution. Waves: 03-01 tracer (governed-pty module + rename + OFF==legacy
differential), 03-02 preflight automation branch + CRLF fix, 03-03 door pins
+ spoof tests + Telegram required source, 03-04 docs/OpenAPI/baseline gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…with OFF==legacy differential

- add hub/src/dispatch/governed-pty.ts: flag reader, eligible-actor rule, four governors (kill_switch/lifetime_counter fail closed), test-only DI, governedAutomationPtyGate
- export isDailyTokenCapEnforced from gates.ts
- telegram dispatch composes governedAutomationPtyGate
- add differential test against a frozen legacy oracle and flag-ON telegram cases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tionPtyGate

- ask and work compose governedAutomationPtyGate with unchanged server-inferred resolvers
- pin that an external-ask actor stays blocked with the flag ON and every governor passing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rnor semantics

- remove humanOnlyPtyGate from gates.ts (hard rename, no alias); keep AUTOMATION_ACTORS and humanOnlyRejectsActor
- human-only-guard test targets governedAutomationPtyGate with the flag pinned OFF
- pin governor order, enabled rule, fail-closed slots, throw, non-PTY, DI-only and no-alias

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…preflight

- automation actor refused before any DB gate unless the governed-automation flag is ON
- flag ON: spend chain then governors, prefixed reasons; throw/hang stay fail closed
- human chain and scanned gates literal untouched

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… humanInteractive guard CRLF-safe

- no governor in either chain constant; human turn passes with fail-closed governors and flag ON
- split source on optional CR plus LF (Phase-2 MINOR-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed when missing

The webhook and the doctor replay assert source 'human' at the authenticated
boundary; a call without a source is refused on a pty-interactive session
instead of defaulting to human (D-09 iv).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and preflights as human

- term.input with source/actor extras -> preflight actor human, forwarded keys exactly type/session_id/bytes
- term.attach_file stays preflight-exempt (D-10) and forwards no source/actor keys

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outing, privileged callers, flag/DI, exemptions

- emitter allowlist (ws/client.ts, telegram/transcript/pty-inject.ts) with non-vacuous self-check
- supervisor canary: user_message/permission_response never touch the PTY, term.input does
- caller pins for automation/human actor, preflight, injectPtyKeystroke, dispatchToSession
- flag/DI literals confined to dispatch/governed-pty.ts; actor-inference and Telegram source pins
- EXEMPTIONS record: injectPtyKeystroke and term.attach_file

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-2 mechanism claims, Phase-4 hand-off

- usage-cost.md: PTYCAP Phase 3 section; structural reason automation cannot reach the PTY
- CLAUDE.md: REMO_GOVERNED_AUTOMATION_PTY env bullet, docs-map row, invariant, token-cap fail-closed fix
- ROADMAP: Phase 4 hand-off note (no SC change)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate rename across docs and SESSION-ASK spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onPtyGate; finish rename in hub/src comments

Comment-only source changes; docs:sync regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…59 fail 0) with phase-3 accounting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nity) ceiling config

maxInjectsPerHour() coerces non-finite env to the default 4, so the governor
passed an invalid value. Add isInjectRateConfigured() (raw validity, governor
only; legacy gate behavior unchanged) and align docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…am/doctor.ts)

doctor.ts takes dispatchToSession as an injected dispatch param, so the bare
call-site needle missed it. Pin importers instead (webhook + doctor), and
assert the doctor replay call passes source: 'human'. Docs name doctor.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pass)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eight executable plans (G-1 emitter fix first, tracer-led controls core, counting expansion, PTY door + governors, owner surfaces + 60% alarm, kill-switch coverage, spawn-site guards, e2e + docs + 20M cap last), VALIDATION map with source coverage audit, and 04-FORKS.md recording planner forks for panel confirm or overturn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply the locked fork-panel verdicts and the plan-check fixes: heal route classified machine (F-2), separate final halt check before send (F-3), release resumes every scope and once-task re-arm (F-4), 80 percent lifetime warning page (F-5a), triage key per application (F-5b), one atomic lifetime + de-duplicated rate-union reservation with routine_run_log.inject_token (F-6), zero new skipped tests with the DB proof moved into 04-02 via an existing e2e test body (F-8). 04-02 split into 4 tasks, 04-03 gains a run-log token task, 04-07 gains a heal task, 04-08 reduced to docs + cap-last-code-commit. Includes 04-FORKS-PANEL.md and 04-PLAN-CHECK.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ima and positive deltas

- G-1/D-15: records of one assistant message share message.id and carry cumulative usage; count once
- key = message.id, fallback uuid; negatives clamp to 0; maxima never decrease; replay emits nothing
- FIFO bound SEEN_UUID_CAP kept; six new fixtures in pty-usage-tail.test.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
finedesignz and others added 28 commits October 13, 2026 18:05
- registry.registerInternal and catchup.runOnce skip task_qc rows unless the flag is ON (4 lines)
- end-to-end flag-off suite: load, register, replace, catchup, fire, seed, email
- harness gains listEnabledTasks; seed.ts added to the closed import allowlist

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- global, user and env halts skip with automation_halted:<scope>, no engine, no email
- isAutomationHalted receives an undefined sessionId; a session-scoped halt does not stop the run
- release resumes; a malformed row with a session_id fails closed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an summary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ncy helper

- runTaskQcSchemaChecks: CHECK re-apply with task_qc rows, per-user partial unique index, NULL session anchor, findings constraints, dedupe, ENGINE claim, cascade
- runTaskQcEngineChecks: report-only row snapshot, three allowlisted fixes with executed rollback SQL, writer row lock, in-flight refusal, atomic run claim
- no test() calls: invoked from existing gated tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2e bodies

Zero new tests, zero new skips; executes in Woodpecker postgres:16 only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rationale in the specs

- 08-SPEC TQC-07 rewritten as deterministic/no model; knob table, TQC-03 (three kinds), DDL updated
- 09-SPEC AR-3, CQ-04, knob table and CQ-05 examples follow the three-kind allowlist
- 08-CONTEXT gains dated Planner decisions; 08-FORKS/CONTEXT cost rationale corrected
- task-qc-spec-supersession.test.ts enforces it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/scheduled-tasks.md: Task QC section (detectors, three fixes, arming order, bounds, digest, API, seed)
- CLAUDE.md: four env knobs, constants-not-knobs, no token cap, docs-map row
- docs/usage-cost.md: taskqc:<userId> counter key family

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
executeChain now reads the child row through the DAL to refuse a task_qc child (plan 08-03),
so the DB-free smoke hit Postgres (ECONNREFUSED) and broke the check-baseline fail_max gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…added to tsc

The phase gate requires the tsc error count not to exceed the 427 recorded before the phase.
Types only (cast through unknown, as any on test json bodies, ts-expect-error on cache-bust imports).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d (skip stays 259)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eck (codex MAJOR 2, MAJOR 3)

MAJOR 2: every QC transaction now arms the remaining pass time as
SET LOCAL statement_timeout before any lock wait and before each read
statement; a database cancellation (57014/55P03) is treated as the deadline
passing (truncated, unpersisted 'deadline' proposal, fix tx rolled back whole).
The app-uuid resolver is raced against the deadline. Engine lookups and
proposal inserts run in armed transactions.

MAJOR 3: the safe-fix writer takes pg_advisory_xact_lock on
'taskqc-session:' || session_id between its row lock and the in-flight probes;
scheduled_task_runs gets a BEFORE INSERT trigger taking the same lock (additive,
CREATE OR REPLACE, schema-lint pragma), so a run for any task on the session
either commits before the probe or waits for the writer. The task row lock is
now FOR NO KEY UPDATE so it cannot deadlock against the FK key-share of a
concurrent run insert (lock order row -> session on both sides).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(all three seats, MAJOR)

PATCH on a non-task_qc row with task_type 'task_qc' now returns 400
task_qc_machine_seeded before any write, matching the POST guard, so the
generic path can no longer mint an auditor row that skips the hourly cadence
floor and the system-row protections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ness MINOR 4)

target_offline, no_online_supervisor, launch_pending and the daily cost cap
reasons are not a task defect and no longer produce a propose_disable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y at restart or next edit (correctness MINOR 3)

The writer corrects the stored column only; the cron job reads its timezone
when armed. The applied block now states that instead of implying it is live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er comment (correctness MINOR 2, 6)

Proposal rows get digest_sent_at (additive column). A same-day conflict
re-returns the row while it is undelivered; the sender marks rows delivered only
after a successful digest send, so a failed send or a same-day re-run no longer
drops findings, while an already-delivered row is still not re-mailed. The
sender comment now states the real dedupe (owner-local day) and documents the
mid-pass kill-switch window for the read-only digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, session lock, delivery marking, PATCH guard)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n rows instead (codex round 2 MAJOR 1, 2)

The round-1 BEFORE INSERT trigger put an advisory lock on every session-bound
scheduled_task_runs insert even with Task QC off, and its NULL-session skip
plus FOR NO KEY UPDATE reopened an own-run race for supervisor runs.

schema.sql now drops the trigger and function if present (idempotent, no lock
on a converged boot). The safe-fix writer takes the task row FOR UPDATE (a run
insert's FK key-share conflicts with it, session-bound or NULL-session) and the
target sessions row FOR UPDATE (scheduled_task_runs.session_id references
sessions) before probing, so run insertion is the unchanged pre-feature SQL.

Tests: task-qc-run-insert-untouched (insert SQL pinned, no trigger),
task-qc-in-flight (lock order, null-session), task-qc-schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OR 2, still open)

- boundedBegin: the wait for a pooled connection is raced against the deadline
  for the read, proposal and fix transactions; a transaction that acquires its
  connection late rolls back before issuing any statement. Only acquisition is
  raced, so a write transaction is never abandoned half-way.
- applyFix re-arms statement_timeout with the time left before EVERY statement
  (lock, session lock, probes, UPDATE, audit insert), not once per transaction.
- haltCheck is raced against the deadline; an unanswered check is treated as
  not cleared to write (deadline), never as not halted.
- The sender shares one run deadline with the engine: the digest email request
  is capped to the time left (sendEmail timeoutMs), nothing starts past it, and
  the delivery mark runs in a transaction with its own statement_timeout.

Tests: task-qc-deadline (slow pool for read and fix transactions, per-statement
re-arm, deadline between UPDATE and audit insert, never-answering halt check),
task-qc-tracer (email bound, mark timeout, no send past the deadline).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ex round 2 MAJOR 3)

A digest cuts findings at the 200-finding and 100k-char caps, but the sender
marked every recorded row delivered after any successful send, so a cut
finding stopped being re-reported that day. buildDigest now returns the
findings it rendered, the engine returns recorded rows keyed by finding
(replacing recordedIds), and the sender marks only rows whose finding was shown.

Test: task-qc-tracer size-cut digest marks exactly the rendered rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(codex round 2 MINOR 5)

Detection reads target_id ?? session_id, but the rendered rebind command
guarded only session_id, so a target-only stale binding made it a no-op. The
command now rewrites whichever column holds the stale id (as the safe-fix
writer does) and matches on either.

Tests: task-qc-proposals, task-qc-digest-escape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the history bound (codex round 2 MINOR 4, 6)

SKIP_FOREVER compared reasons after the 120-char render cap, so two long
reasons differing later grouped; it now compares the full normalised reason and
truncates only the rendered evidence. The detector window now filters ignored
statuses (cancelled, non-terminal) before taking the newest N, and the history
read ranks counted and ignored statuses in separate partitions so the per-task
bound can no longer be consumed by ignored runs (the ignored partition still
supplies the NEVER_FIRED existence signal).

Tests: task-qc-detectors, task-qc-load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…icity, hard deadline, rendered-only delivery marking)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SC1-5 and all fork-panel amendments verified against code and tests;
1 open MAJOR (sender whole-run deadline) re-confirmed, PG-only proofs CI-pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… acquisition, in-callback recheck)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agentautofix-fixer

Copy link
Copy Markdown
Contributor

AgentAutofix AI Review

claude

PASSED

  • warning hub/src/dispatch/gates.ts: humanOnlyPtyGate removed - confirm all call sites migrated -- This diff deletes the humanOnlyPtyGate function entirely from gates.ts and introduces governedAutomationPtyGate in governed-pty.ts. hub/src/ask/dispatch.ts is shown migrated to the new gate, and docs reference work/dispatch.ts and telegram/dispatch.ts as also using governedAutomationPtyGate, but the actual code diffs for those two files are not present in the (truncated) diff supplied. If those files still import the now-deleted humanOnlyPtyGate from gates.ts, the build would break. Recommend confirming work/dispatch.ts and telegram/dispatch.ts were updated in the full PR before merge.

codex

PASSED

No findings.

agy

advisory -- not part of the gate decision

PASSED

No findings.

Result: Both AI reviewers passed

Reviewer models (requested pin, not independently confirmed):

  • claude: claude-haiku-4-5-20251001 (requested pin, not independently confirmed)
  • codex: gpt-5.5 (requested pin, not independently confirmed)

@agentautofix-fixer

Copy link
Copy Markdown
Contributor

AgentAutofix did not auto-merge this PR.

Blocking reason: CI is failing on the reviewed commit: ci/woodpecker/pr/qc

Reviewed commit: eadc987. A human needs to resolve this (or merge the PR manually). This comment is updated in place when the reason changes; it is not re-posted per retry.

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