process_audit_verdict now calls validate_signer_separation
before any state save or ledger write (previously the function
had no production caller on this path: it only ran inside
collect_failed_checks via decide_pushback, which itself has no
production caller). Signer is bound to the authenticated
caller -- a body signer differing from the caller is rejected
-- and executor/coordinator_approver/qa_tester must be
declared and distinct from the caller; missing field,
self-audit, or forged signer raises ValueError, mapped by
_loop_run to HTTP 400 with no side effects. No new route.
Honest limits are in the helper docstring: declared-identity
enforcement only (task rows store no authoritative executor /
coordinator / QA identity; task executor is a backend, not a
person), and audit-fix reaudit verdicts are out of scope
(the fix poster is not necessarily the re-auditor, so binding
signer to the fix poster would be a mis-binding).
(cherry picked from commit 56dce5165f4d08f38fe1730b9eea3b78f90cb97d)
Reconcile-port: handler_lifecycle.py docstring hunk redirected into cloud/server.py audit-verdict handler (target keeps 6-mixin split; enforcement itself is in audit_loop_wiring.py and applied cleanly).
Server POST /tasks/{id}/merge no longer has record/enforce/off:
hard_mode_from_env and HARD_MODES deleted (not re-defaulted),
handler always evaluates the hard gate, env CLOUD_MERGE_GATE_MODE
and body merge_gate_mode are ignored server-side. The record
'no-evidence passes' and 'unprotected+confirm passes with
merge_gate_risk' server paths are gone with the dead risk audit;
no production caller justified keeping them (only production
caller of the non-hard evaluator was the merge handler itself;
audit_loop_wiring already used the hard gate). cloud/merge_gate
pure functions keep their #209 semantics but are documented as
non-server: they are no longer wired to any endpoint.
Server-level record/enforce/off tests rewritten to the new
behavior (renamed, not deleted silently); pure-function tests
unchanged.
(cherry picked from commit 08e75edbd6d04701965c5db951fa054cfbb75bb0)
Reconcile-port: handler_lifecycle.py hunks redirected into cloud/server.py _merge_task (target keeps 6-mixin split; mixin file does not exist here); handler_support.py kept at HEAD (its A1 hunk is a docstring of _validate_hard_gate_body, ported to server.py copy); test mock paths cloud.handler_lifecycle.* rewritten to cloud.server.*.
- cloud/evidence_binding.py: bind/validate/nonce single-use store;
release endpoint consumes nonce on first validated submission,
replay -> 409 evidence_replayed, stale/future -> 409
evidence_stale, partial -> 400, absent -> old path + label.
Binding blocks exact replay and stale evidence, NOT restamping
(unsigned evidence cannot prove content freshness).
- cloud/quota.py docstring: actual-only is the v2 integration
position (already converged in v1 base via fef09ae, NOT in
main); original PR semantics (#86 all-rows vs #90 actual-only)
still pending coordinator confirmation at merge time.
- cloud/ledger_onboarding.py: DDL checklist referencing
PG_DDL/PG_MARKER_DDL constants, read-only verify_ledger_ready
and ledger_setup_report; default stays OFF, PG DDL stays an
operator action (never claimed as executed).
- cloud/ledger_outbox.py + worker hook: ledger write failures
go to a JSONL outbox in addition to the log and can be
replayed; task outcome still unaffected (rule 7 kept),
audit fail-closed untouched -- asymmetry documented, not
silently changed.
- cloud/gate_verification.py: read-only verifier over an
injected reader (match/mismatch/skipped); live wiring in
merge/release is NOT possible on the current contract (no
repo/ref in body or task row, no guaranteed trusted token),
proven in the module header; no fake verification claimed.
- cloud/tests/test_v2_evidence.py: 18 new tests, all passing.
(cherry picked from commit bd85e33cd5e846fa6ee5d7e2440182dbd9c758b0)
- db: translate INSERT OR IGNORE ... SELECT to ON CONFLICT DO NOTHING;
register task_audit_loop_state (task_id) in _OR_REPLACE_PK (direct port).
- server: _backfill_default_quotas uses SAVEPOINT containment, no longer
commits the caller's transaction (direct port; callers are init_db only).
- worker: NOT ported — integration _coord_child_rows has no column-fallback
retry loop, so the InFailedSqlTransaction cascade site does not exist here.
- tests: rewritten for the integration base (no cloud.audit_loop /
_coord_audit_loop_merge here); real-PG cases skip without DSN/driver.
(cherry picked from commit 79e6843610fcd275af874e7b4afc57af7ffa579e)
- cloud/audit_scorecard.py: pure stdlib scorer, structured evidence in,
7 dimension scores (0-10, 1 decimal), basis, missing/untested items,
overall pass only when all 7 >= 9; unknown -> None, never 0/10/estimated;
fixture/viewport/code-review evidence -> untested, never pass;
thresholds all test_default_unapproved, user_approved only on explicit input.
- server.py: GET /audit-scorecard read-only, same auth/member scope as
/metrics, no DB writes, no ledger writes, no pushback/reaudit triggers;
on main base it honestly reports unknown/not-enforced, overall not_pass.
- docs/audit-scorecard.md: spec, formulas, threshold table, unknown
handling, overall rule, 10-to-7 merge mapping, relations to
#238 metrics-report, #244 sampling, #260/#261 loop, and a system-computed
example (current state, overall not_pass) clearly separated from the
previous judgement estimates.
- tests: 12 new items pin all-pass, one-below-nine fails, average 9.1
with one 4 still fails, unknown None, fixture untested, conditional
not a pass, thresholds unapproved, endpoint 401/200 read-only.
- suites: cloud 1442 OK (skipped 4), top-level 105 OK, deploy 39 OK.
Server-side execution for the track-1 pure driver (#260), no driver
semantics changed: audit verdict / fix completion / correction /
rebuild confirmation endpoints persist LoopState in audit_loop_state,
write every step through the #241/#247 ledger path (ledger first,
state second, fail-closed), derive fix/rebuild subtasks, and block
merge/release via the existing #247 chain reader. POST
/tasks/{id}/release follows the #259 contract (global-admin write,
fresh hard-gate evidence, merged status, multi-source 409 with no
audit write on block); deploy.py stays outside task gates.
- cloud/attempt_ledger.py: separate task_attempts table (not tasks
columns) with per-attempt rows, MAX+1 attempt_no, append-only
intent; interrupted rows kept but never counted; is_defect is
explicit, never inferred from failure; first defect category
wins; no-ledger tasks stay unknown (None), never backfilled.
SQLite DDL is CREATE TABLE IF NOT EXISTS (idempotent); PG_DDL
is the verbatim manual step for production Postgres, kept out
of POSTGRES_SCHEMA on purpose (migrations coverage untouched).
- cloud/server.py GET /metrics-report: aggregates the ledger
into compute_metrics records; missing ledger table degrades to
unknown rates, not an error. Cost dual-column code untouched.
- cloud/metrics.py: docstring only (ledger is now the source);
compute_metrics logic and all #238 semantics unchanged.
- Tests: 16 ledger tests (store/aggregate/metrics/PG adapter
evidence) + 3 endpoint tests (real rates, old task beside
ledger task, interrupted status non-terminal).
The #241 import resolution deleted the whole 'from cloud.cost import
CostStore, dual_column_view' line to drop the unused alias, which
also removed the CostStore import that _quota_snapshot needs; every
task creation then failed closed with 503 quota_check_unavailable
(4 EndpointChainTestCase failures in test_audit_wiring). Restored
as a plain CostStore import; dual_column_view stays available in
cloud/cost.py for #241's own tests.
- cloud/arch_conformance.py: contract-version (+1 only, approved
change record), forbidden-directory (own dir only, .. and symlink
escapes resolved), forbidden-import layer bans audited against the
baseline (foundation/execution/driver, server sqlite3 Row adapters
deliberately allowed), G2 frozen-stack swap guard; skill-project
one-call check.
- cloud/metrics.py: first-pass/defect/rework rates over terminal
tasks, unknown rates as None (never 0), #212 dual-column per-task
cost (total None when incomplete), pure feedback rules
(tighten/route-with-extra-audit/recalibrate, auto_apply False),
golden-set gate: unapproved sample set is never decision-ready.
- GET /metrics-report (authenticated, member-scoped, same as
/metrics): attempts/defects are None until an attempt ledger
exists, so rates stay unknown rather than fabricated.
- Tests: 29 arch + 15 metrics + 3 endpoint, each check with
positive and negative cases; repo-level scan pins zero layer
violations on the baseline so a future violation fails CI via the
existing host-verify discover run (no workflow change).
Track 3 decisions (audit_pushback) now really block the server merge
endpoint and really land on the track 4 hash chain, via the new
cloud/audit_wiring.py translation layer:
- POST /tasks/{id}/merge ANDs the chain pushback block
(merge_blocked/release_blocked/blocked_by=audit_pushback/round/
failed_checks, replayed per module+contract version) with the
hard gate's five checks in every gate mode; either side blocks
-> 409, status unchanged, audit_pushback_blocked appended to
merge_gate.blocks. Ledger reads that fail (other than a missing
audit_ledger table) fail closed to 409.
- Every pushback/fix/re-audit/rebuild/escalation step is written
to the ledger with track 3 fields authoritative and track 4
storage authoritative; CONDITIONAL maps to fail at the boundary
with the original preserved, history never rewritten.
- Hard-gate 409 blocks codes feed pushback packages machine-first
(branch protection survives as its own extra check).
- Re-audit QA guard: a claimed pass with un-rerun or failed QA
links never reaches the chain.
- Track 4 compat: one audit_wiring_event action for escalation /
blocked-quota / audit-sample kinds, counted in the timeline.
- Resolved tension (documented in audit_wiring.py): a meta-review
correction to fail re-blocks at once, but a correction to pass
does NOT unblock by itself -- track 3 schedules a re-audit,
it does not release the gate; track 4 failure counts still use
effective verdicts, so the two views may disagree while a
re-audit is pending.
Tests: cloud/tests/test_audit_wiring.py (18 items), incl. a real
HTTP end-to-end chain (fail -> 409 -> fix -> re-audit pass ->
200) and a separate QA-link chain test.
- CostStore: per-model breakdown, report_by_creator (JOIN tasks.created_by),
dual-column reports with priced_usd_micros, unpriced_models listed
separately and a billing note (proxy price, not the relay bill);
_report itself untouched, old exact-key tests keep passing.
- Hard cutoff: cost.quota_cutoff_reason is the single USD/unpriced-cap
decision; worker._quota_exceeded now delegates to it (identical
messages); claim gate, turn boundary and poll kill stop spend and
record interrupted + quota reason + audit; auto-resume re-checks
live quota before requeueing.
- Approval timeout (G7 ops metric): cloud/approval_timeout.py —
default stays 600s, >20% timeout rate recommends 1800s, <5% keeps,
mid-range keeps; stats + recommendation + info log; wired into
/metrics (member projects, no log spam) and a global-admin
/admin/ops/approval-timeout endpoint (sweeps overdue first).
- Server: task/project cost endpoints return dual-column reports;
new GET /users/me/cost and GET /admin/cost-by-creator.
- Tests: cloud/tests/test_cost_quota.py, 10 tests.
GET /full-auto/status, GET /full-auto/config-plan, POST
/full-auto/validate-action|merge-gate|ci-fix|release-plan|
rollback-plan, POST /full-auto/execute (global admin only,
FULL_AUTO_ENABLED + verified installation + branch protection
verified via GitHub API, else 403 + audit), GET /full-auto/audit.
Guards reused from cloud/full_auto.py; token only in Authorization
header; audit via audit_record redaction into AuthStore audit_log.
Only pure computation/validation sub-blocks of _handle_github_webhook,
_create_followup, _deliver_to_thread and _task_chat_history are extracted;
all DB writes, audits, external calls and side-effect blocks stay in the
callers, error strings/codes and response shapes verbatim. _deliver_to_thread
uses the shared goal classifier only for /goal input: its non-goal terminal
path still goes to CAS revive (not 409) and its merged path keeps its own
wording. New tests: test_longfunc_remaining.py (15).
_continue_task 226->168 via _validate_continue_options;
_task_wait 158->139 via _validate_wait_params;
_task_thread_read 187->152 via _summarize_trace_entries;
_metrics 123->104 via _summarize_metrics_tasks.
No behaviour, message, status or route change; error strings verbatim.
New tests: test_longfunc_extra (9). Cloud full suite: 1439 OK, skipped=4.
Follows #229 pattern; must merge after #229 and be rebased/checked then.
Move the /tasks/* sub-route out of _dispatch into _dispatch_task_route
(query passed explicitly; the files/tree/thread branches use it), move
the push-subscription DELETE branch out of _dispatch_project into
_project_push_subscription_delete, and turn the goal-validation block
of _create_task into the pure module function
_validate_task_goal_options (returns error string instead of sending
400, caller sends it unchanged). Statements are moved verbatim except
indentation and the goal send+return -> return-error transform; no
behaviour change. server.py module split intentionally not done: it
remains a larger, higher-risk refactor.
Add test_longfunc_split covering the pure goal validator's success
paths and all eight error strings.
- ensure_workers_table: new tables include host TEXT, pid BIGINT,
address TEXT; old tables gain them via _ensure_hostpid_columns
(PRAGMA column check, missing-only ALTER, old rows keep NULL,
never fabricates host/pid). SQLite and Postgres share this path
(PG via the db adapter's PRAGMA translation); POSTGRES_SCHEMA
deliberately still excludes workers, which is volatile heartbeat
data in UNMIGRATED_TABLES (adding it broke the P2 migration
alignment tests, verified and reverted).
- heartbeat/register: given host/pid/address refresh, omitted
values preserve the old row; address has no derived source.
Local worker passes host_name + own pid; HTTP heartbeat only
stores caller-supplied values, never the server's own.
- classify_stale_workers: kill only when age strictly exceeds the
threshold AND host matches local host AND pid is proven dead;
equal threshold, foreign host, missing pid, or live/unknown pid
are list_only. Classification only, never kills, never matches
by name/prefix, never touches infra containers.
- cloud/merge_gate_hard.py (new): pure hard gate; mode resolution is
env-only (unset/invalid -> hard, fail-closed); five checks plus
branch protection must be protected; unknown/unprotected block
even with explicit confirm; no-evidence blocks (fail-closed);
directory check reuses #209 check_directory_isolation (v0.3 rule);
blocks codes + merge_gate_blocked audit detail are the track-3
pushback interface (no pushback state machine in this track).
- cloud/server.py: POST /tasks/{id}/merge uses hard mode by default;
record/enforce/off only when explicitly set via env (preserving
#209 semantics); body merge_gate_mode is ignored server-side so
callers cannot downgrade the gate; every gate block writes a
merge_gate_blocked audit before returning 409, status unchanged;
hard-mode body type errors return 400.
- docs/merge-gate-branch-protection.md (new): protection payload
(PR + strict status checks + no force-push + no deletion +
enforce_admins), empty required_checks = unconfigured, Pro/public
prerequisites, 403 causes; states protection is NOT claimed enabled.
- SKILL.md (gate-mechanism wording only): four checks -> five checks;
body + coordinator + client appendix gate paragraphs updated to
default hard, merge_gate_blocked audit, payload doc reference.
- tests: 31 new hard-gate tests (pure + endpoint + skill text);
#209 endpoint tests now set record env explicitly (their intent);
test_server_merge 200-path calls now carry full hard evidence.
- cloud/merge_gate.py: pure gate; directory check reuses the v0.3
git diff --name-only merge-base command; protection modes
record (default, explicit confirm required when unprotected),
enforce (always blocks when unprotected), off (explicit only).
Confirm covers protection risk only, never directory violations.
Protection status is caller/env-declared only; unknown is a
recorded risk, not a claim of protection.
- server POST /tasks/{id}/merge: applies the gate only when gate
evidence is supplied (or env declares protection); no-evidence
calls behave as before plus a merge_gate field in the response.
Blocked merges return 409 without changing status; confirmed
unprotected merges write a merge_gate_risk audit entry.
- skill_seed.py: whole-directory seeding evaluated and rejected
(cannot preserve tenant deletions; no benefit while the pack
holds only SKILL.md); SKILL.md-only behaviour unchanged.
- software-engineering SKILL.md: gate paragraph added to the body
and to the coordinator + client appendix sections (v0.2 text on
this base; port to v0.3 text on rebase after #199 merges).
- tests: 16 new tests covering gate modes, directory reuse,
body/appendix bidirectional assertions naming the missing side,
SKILL.md-only seeding and tenant-edit preservation.
Long-running website monitoring: new cloud/resident.py layer on top of
the automations claim/quota model (no task state-machine change).
States running/paused/ended; pause preserves the content-hash cursor
and stops polling, resume re-checks from the cursor, end is terminal
(resume after end is 409, a new watch is required). Polling is a cheap
fetch + hash comparison; only a real change creates a queued model
task. Quota exhaustion pauses (quota_exceeded) instead of ending and
keeps the cursor. Driver is the worker run_loop round boundary;
server exposes create/list/get/update/pause/resume/end with audit.
Console UI intentionally not wired this round (see verification
summary for deviations).
Stale bridge state used to end the stream on the first stale read,
so every round gap and every worker restart killed the watch (the
2026-10-30 demo watch died this way when a 15:03 deploy restarted
the worker). Now a stale state switches to a waiting heartbeat
every 5s while the task is still alive (running/queued/paused/
interrupted), and frames resume when the next round's MCP starts
polling. The stream ends only on a terminal task state, a missing
task row, or 10 minutes of continuous staleness. The panel shows
a waiting status and keeps the last frame. Also force-close the
SSE connection on stream end: HTTP/1.1 keep-alive otherwise left
terminal streams hanging with no EOF.
- cloud/tree.py: structured worktree tree (dirs first, ignore
.git/node_modules, symlinks not followed, entry/depth
truncation flags, text render for injection)
- GET /tasks/{id}/tree with /files auth + worktree resolution,
max_depth 1..5, max_entries 1..500, escape 400, missing 404,
cloud/cross-host 409
- MCP get_task_tree translating to the new endpoint
- worker first-round injection of a repo-checkout top-level
snapshot (2 layers, 120 entries) after repo resolution,
labelled as checkout snapshot not worktree; skipped for
coordinator/resume/fork, silent when no host source
- docs/project-tree.md; tests for tree, endpoint, injection,
MCP translation
GET /tasks/{id}/files previously returned 409 for all docker tasks
on the grounds that tenant docker shares /app/work per project.
Production inspection shows the worker creates a per-task
host-worktrees/<task-id> subdirectory under the tenant work dir
(62 such dirs under the wwww tenant), so per-task reads are
attributable after all: the endpoint now resolves tenant docker
tasks via tenants/<project>/work/host-worktrees/<task-id> (local
worktree as fallback), never serves the shared /app/work mount
itself, and still 409s cloud/cross-host tasks. Docs and the
harness2_tasks boundary notes are updated to match.
- Role models: run_improvement_via_tasks gains role_models
(probe/editor/selector/reflector) plus --model-<role> CLI flags;
missing/empty falls back to shared --model, both empty means
server default, so default behavior is unchanged.
- Sweep: run_sweep + examples/harness2_sweep.py manifest CLI with
shared library_root (library restore), per-run failure isolation
and sweep-report.json summary; preflight gates task creation.
- Deliverable assets: GET /tasks/{id}/files for local-executor
tasks (safe path resolution, UTF-8 text <=256KB, listing <=200;
docker/cloud/cross-host 409 with explicit reason, since tenant
Docker shares one /app/work per project and per-task attribution
would misattribute); probe rollout deliverable_files from the
endpoint plus DELIVERABLE FILE reply blocks (endpoint wins on
collision, bad blocks dropped), surfaced in
deliverables/<label>/files/ context and full_result summary.
- Docs: harness2.md comparison table updated for all three items.