Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ef3943
docs(flow): merge SOP audit gate - runtime is dynamic, no wall-clock …
AIOSAI Aug 3, 2026
f409dd5
feat(skills): TG slash relay v1.6.0 - /context from Telegram comes ba…
AIOSAI Aug 3, 2026
1f416c2
fix(trigger): rotation tail loss closed in both log watchers - inode-…
AIOSAI Aug 5, 2026
13f86bf
fix(ai_mail): 6-week 'unreproducible' dispatch failure root-caused - …
AIOSAI Aug 5, 2026
45dcf26
fix(hooks): engine 'complete: 0 hooks' lie - silent gates write no st…
AIOSAI Aug 5, 2026
176aa8e
fix(prax): log retention backup_count 1-to-3 (rotation discarded undr…
AIOSAI Aug 5, 2026
6d90b93
fix(trigger): delete #674-orphaned per-key writers _save_seen_hashes/…
AIOSAI Aug 5, 2026
2d1a5ff
feat(drone): manager-class owner-tier git auth (DPLAN-0281 P1) - owne…
AIOSAI Aug 5, 2026
2922a68
fix(tests): CI fallout from manager-class auth - seedgo Track-E ALLOW…
AIOSAI Aug 5, 2026
886b0e6
fix(tests): Windows CI - router cwd-logging test compared path reprs …
AIOSAI Aug 5, 2026
306a26d
feat(trigger): runaway WARNING tier observe-only per Patrick ruling -…
AIOSAI Aug 5, 2026
7b66ddc
feat(aipass): init update provisions external projects for manager-cl…
AIOSAI Aug 5, 2026
6d8b050
feat(hooks): per-hook log narration env-gated (AIPASS_HOOKS_VERBOSE_L…
AIOSAI Aug 5, 2026
cd7b263
docs(flow): weekly_update template v2 by VERA (Vera-Studio) - Step 0 …
AIOSAI Aug 5, 2026
9fcd94b
fix(ai_mail): wake-back honesty - skipped_manager result tag, no more…
AIOSAI Aug 5, 2026
329ded6
fix(hooks): edit_gate accepts legacy session_number schema - number-k…
AIOSAI Aug 5, 2026
f93d4d9
fix(devpulse): feedback replies report delivery honestly — anonymous …
AIOSAI Aug 5, 2026
60a3fb7
fix(drone): caller detection falls back to registry filename when met…
AIOSAI Aug 5, 2026
73dfe84
fix(devpulse): route feedback delivery outcomes through cli helpers —…
AIOSAI Aug 5, 2026
bcf09d3
docs: fold seedgo routing fixup into tonight's feedback CHANGELOG entry
AIOSAI Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,211 @@ PyPI version — not the changelog header.

---

## [2026-08-04] — manager-class git auth + fleet self-repair day

**fix(drone)** — caller detection derives a project name from the registry
FILENAME when metadata declares none (`AIPASS_REGISTRY.json` → `aipass`,
`VERA-STUDIO_REGISTRY.json` → `vera-studio`); a declared
`metadata.project_name`/`name` still wins, and passports still outrank the
fallback entirely. The old code required a declared name, which AIPass's own
registry doesn't carry — so the framework repo was the one place the fallback
could never fire, and it failed in silence: callers at the AIPass root
(VERA's session, the Telegram scheduler hourly) were `CALLER:UNKNOWN` all
day, which is what stranded the feedback replies above. Found-but-rejected
registries now log WARNING naming the file and reason; the glob is sorted for
deterministic multi-registry resolution; a test asserts a derived name can
never earn git authority (owner-tier reads passports directly). One canary
self-caught and rewritten: the bare-suffix test asserted None, which the
caller's truthiness check made vacuous — now asserts the WARNING. 963 drone
tests green (+8), seedgo 100%.

**fix(devpulse)** — feedback replies report delivery honestly. Live failure
caught by Patrick asking why VERA never heard back: all six of her feedback
messages arrived as `From: unknown` (her session ran drone from the AIPass
repo root, where caller detection finds no passport and the registry
fallback rejects a name-less `AIPASS_REGISTRY.json`), so three replies were
"saved" while delivery silently skipped to `src/aipass/unknown/`. compose.py
1.1.0: an anonymous send is now told AT SEND TIME that replies cannot reach
it (with the run-from-your-branch-dir fix named), and `reply` reports the
delivery outcome — `success()` on delivery, `error()` with the reason on
failure (which marks the command failed: a reply the sender never sees
SHOULD flip the exit code) — instead of claiming success on a thread-only
save. The six stored messages were repaired (sender + reply path) and the
three stranded replies hand-delivered the same evening. 452 devpulse tests
green (+5), compose.py 31/31 seedgo standards.

**feat(drone)** — owner-tier git is earned, not listed (DPLAN-0281, Patrick
ruling: "project owners get git"). The hardcoded `allowed_callers:
["devpulse"]` is gone; a caller holds owner-tier iff all four checks pass:
manager-class citizen, tenant of THIS repo's registry (passport
`citizenship.registry_id` == registry `metadata.id`), listed with `owner:
true`, and presenting its passport from the registry-recorded home
(path-binding, F59 4.2a). devpulse-in-AIPass authorizes through the general
rule — no special case — and any external project's manager gains the same
standing in their own repo once P2 provisioning flips their class. Enforce
by default (all four checks live-verified against real data before
flipping); `AIPASS_GIT_AUTH_MODE=warn` for migration triage. AIPass-flow
verbs (dev-pr/merge/tag/…) refuse honestly in external repos until
translated — commit and sync work there today. Also: `find_repo_root`
recognizes any `*_REGISTRY.json` (external projects name theirs),
dict-authored registries get the same normalization as list-shaped, and the
dead `ALLOWED_CALLERS` decoy died with the list it shadowed. Router
caller-identity honesty landed alongside: a lost identity renders
`[CALLER:UNKNOWN]` plus one WARNING naming the real cwd, and the registry
fallback for external projects is reachable as documented. 44 new tests
(16 canaries + unstubbed-auth module tests), 955 drone green. By @drone,
verified by devpulse.

**fix(tests)** — CI-only fallout from the auth rewrite, caught by the clean
checkout: seedgo's four Track-E tests pinned the dead `ALLOWED_CALLERS`
parity — replaced with one canary asserting no name-based caller list can
reappear; and drone's wrong-tenancy test now pins `AIPASS_REGISTRY` to its
fixture — `find_registry`'s cwd walk deliberately skips credential-failing
registries, so the mismatched fixture was passed over and resolution fell
through to the real registry locally (right wording, wrong reason) but to
not-found in CI, where `AIPASS_REGISTRY.json` is gitignored-absent. Seedgo
1304 green, drone 955 green. A third, Windows-only: the new router
cwd-logging test substring-matched path reprs — `str(tmp_path)` has
backslashes while the logged arg renders `WindowsPath('C:/...')` with
forward slashes — now compares Path values, separator-agnostic.

**fix(trigger)** — rotation tail loss closed in BOTH log watchers. The old
`size shrank → reset to 0` rotation handling silently skipped every line
between the last read offset and the rotation cut — worst exactly during
incidents, when the unread tail is largest; a second defect seeked stale
offsets INTO the fresh file, reading garbage fragments. Now: inode identity
recorded beside the offset, rotation detected by inode change, and the
rotated-out file's unread tail drained before moving on (inode-matched, so
never a stale backup re-fired). Falsy/unknown inode degrades to old
behavior. Found by @trigger while disproving another branch's rotation
claim. 698 trigger tests green.

**fix(hooks)** — edit_gate's newest-first guard no longer hard-blocks
legacy `session_number` branches from ever writing session memory (found by
VERA — the gate was stricter than the schema the rest of the fleet still
honors, with no compliance path). Two halves, both proven load-bearing by
staged canaries: a number-key alias (`number` wins over `session_number`
when both exist) so legacy arrays stay *guarded*, and an unreadable-schema
pass-through so an unrecognized future schema degrades to the
ordinal-independent ordering check instead of a permanent lockout. Block
messages now name the accepted keys. Live-proved through the real Claude
bridge: legacy prepend exits 0, tail-append and number-reuse still exit 2.
1335 hooks tests green (+9). By @hooks, verified by devpulse.

**fix(ai_mail)** — wake-back no longer claims "woken" when the manager gate
skipped it (found by VERA in Vera-Studio field telemetry after her manager
flip; diagnosis exact, line for line). The gate's bool means "the dispatch
did what it should," not "an agent was woken" — a manager returns True
having deliberately woken nobody, and `_wake_sender` read that as woken.
New `skipped_manager` result tag keyed on the status object's structural
step (not prose-sniffing — substring matching is what let this hide),
docstrings now tell the truth about managers, and the unreachable @daemon
exception on wake-backs is explained in place. Gate behavior untouched.
839 ai_mail tests green (+9, canary-checked both directions). By @ai_mail,
verified by devpulse.

**docs(flow)** — weekly_update playbook template v2, authored by VERA
(Vera-Studio) from her PPLAN-0017 run and landed from flow/dropbox: new
Step 0 reads the live subreddit for the last posted number before anything
else (an empty playbook is not evidence its post never fired — trusting one
cost a delete-and-repost of an immutable Reddit title), and a cold-tested
"Driving Chrome" section including the `pgrep -x chrome` correction
(`pgrep -f google-chrome` false-positives on the caller's own command
line). First cross-project template contribution.

**feat(hooks)** — hooks_engine.log per-hook narration demoted out of the
default view (ruling delegated by Patrick, decided by devpulse: quiet noise
at the source, never mask it). prax's SystemLogger has no debug(), so
engine 1.2.0 gates the four per-hook narration sites (fire, complete,
skipped-disabled, budget) behind `AIPASS_HOOKS_VERBOSE_LOG=1` — silent by
default, restorable live, read per call. Lifecycle INFO, every WARNING and
ERROR, and engine.jsonl untouched. Measured under fleet load: 1865 of 1869
lines demoted (~99.8%); the 4 survivors were legitimate git_gate blocks.
1326 hooks tests green (+5, suppression canary-checked), seedgo 100%. New
README "Two Log Streams" section. Flagged upstream: SystemLogger's missing
debug() is a real prax gap. By @hooks, verified by devpulse.

**feat(aipass)** — `aipass init update` provisions external projects for
manager-class git (DPLAN-0281 P2). New `init/git_auth.py`: plans every
repair BEFORE writing (a refused run leaves the project untouched), mints
registry `metadata.id`, backfills the owner citizen's
`citizenship.registry_id`, flips builder→manager, records the branch path —
then `verify_git_auth()` independently re-reads disk and re-derives all
four owner-tier checks. Refuses honestly instead of guessing: no owner
marked, more than one owner, root-ish recorded paths (@drone's guardrail —
at-or-under binding would degrade to repo-wide), paths outside the repo, or
missing passports. `--dry-run` prints the plan, writes nothing. Canaried
against drone's real P1 gate: repaired fixture authorizes, un-repaired
refuses on class, a passport copied to a non-recorded dir refuses on
path-binding. 934 aipass tests green (+37), seedgo 100%. By @aipass,
verified by devpulse. P3 (run it on Vera-Studio live) is next.

**feat(trigger)** — runaway WARNING tier is observe-only (Patrick ruling:
"observe only is good"). WARNING runaways record with full fidelity —
alerts.json, decision log, per-file cooldown — but no longer email or wake
anyone; CRITICAL keeps its bypass-all-mutes wake path untouched. New
decision outcome `observed` (not `suppressed` — it was recorded; not
`delivered` — nobody was told), and a WARNING now records even with no
email callback, where it previously early-returned recordless. The accepted
cost is written into the module docstring: a sustained sub-CRITICAL leak
pages nobody by design. 707 trigger tests green (+13), reverted-split
canary fails 8, five NO-OVERREACH tests pin the CRITICAL path. By @trigger,
verified by devpulse.

**fix(trigger)** — follow-up: the seedgo unused_function gate (CI red on
PR#727) caught `_save_seen_hashes`/`_save_log_positions` orphaned since
#674's coalesced flush — only tests still called them. Deleted rather than
wired-to-nothing (the None-watcher "gap" doesn't exist: the flush merges
with existing on-disk JSON, preserving positions untouched). Their test
blocks repointed at the real write path `_flush_trigger_data`, and got
stronger: the old write-error tests asserted nothing; the replacements
assert the warning reaches the logger, canary-checked three ways. 694
green, trigger audit back to 100%.

**fix(ai_mail)** — the 6-week "unreproducible" dispatch failure
(2×468-adjacent fingerprints, 44 occurrences) root-caused and reproduced on
demand: sender identity resolves from AIPASS_CALLER_CWD, so running drone
from a non-branch dir (repo root) fails detection — while the error printed
the target's perfectly-valid cwd, sending every prior investigation passport
-hunting. The refusal is CORRECT (silent cwd fallback would forge sender
identity); the fix is diagnostic truth: the error now names the env var,
the walked path, and that process cwd is informational. Fingerprint prefix
preserved for medic grouping. 4 canary-checked tests, 830 green. By @ai_mail.

**fix(hooks)** — engine "complete: 0 hooks" lie fixed: silent gates write no
stdout, so len(outputs) reported 0 on 97% of dispatches while gates fired
normally. Now counts executions; hooks_with_output added. Runaway
hooks_engine.log alert itself verdict'd NOT a hooks bug — fleet load
(24 claude processes, load 32 on 4 cores). engine.py 1.1.1, 4 canary tests,
1321 green. By @hooks.

**fix(prax)** — log retention: backup_count 1→3 (rotation was discarding
history the watchers hadn't drained; ~28MB ceiling accepted), and dead
prax_logger_config.json read-keys found/wired (settings never matched what
load read). By @prax under @trigger dispatch. 1084 green.

## [2026-08-02] — TG slash relay: /context fired from Telegram comes back to the chat

**feat(skills)** — CC informational slash commands now round-trip from
Telegram (Patrick ask: stop pick-and-choosing which builtins work remotely).
The bot injects an allowlisted informational command (`/context`; extend via
`informational_commands` config) as raw text — no relay prefix, or CC would
read it as prose — then a daemon-thread watcher tails the CC transcript from
the injection baseline and relays the command's stdout back to the chat as
HTML `<pre>` chunks. Local commands produce no assistant turn, so this path
deliberately writes NO pending file and starts NO heartbeat (nothing for the
Stop hook to strand — the stuck-pending lesson applied, not relearned);
90s timeout edits the placeholder to an honest failure. Scope-guarded twice:
watcher only starts from TG-inbound handling and the scan is bounded to
lines after the baseline — a desk or remote-control `/context` can never
surprise-echo to the phone. Found en route: current CC emits `/context`
twice (ANSI TUI panel + clean-markdown isMeta twin); the twin is preferred.
`/cost` verified-not-assumed and left OUT (zero invocations exist on this
machine to pin its shape). Side-effect passthrough (`clear`/`compact`/
`prep`/`memo`) byte-identical behavior. 51 new tests (canary-checked: each
guarantee broken in turn, tests bite), 1010 telegram green, seedgo 100%.
Built by @skills; live-proven end-to-end including a real Telegram hop.

## [2026-08-02] — install ends with hooks alive: setup enrolls itself; hook test runner stops ghost-arming live sessions

**feat(setup)** — setup.sh now enrolls the repo it just installed in the hook
Expand Down
26 changes: 21 additions & 5 deletions src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# =================== AIPass ====================
# Name: dispatch_monitor.py
# Description: Agent Lifecycle Monitor
# Version: 2.1.0
# Version: 2.2.0
# Created: 2026-03-02
# Modified: 2026-07-31
# Modified: 2026-08-04
# =============================================

"""
Expand Down Expand Up @@ -93,12 +93,14 @@ def _connect_broker(repo_root: Path, branch_name: str) -> socket.socket:
def _wake_sender(sender: str, branch_email: str, exit_code: int, lock_file: str) -> str:
"""Wake the dispatcher back after target completion.

Any citizen sender gets woken back (same availability checks as
normal wake — interactive session, active lock, depth cap).
Builder-class citizens get woken back, subject to the same availability
checks as a normal wake (interactive session, active lock, depth cap).
Managers never are: wake_branch's manager gate delivers the mail and skips
the wake by design, so a manager dispatcher is only ever mailed back.

Returns a result tag for the dispatch_wake.log:
success, blocked_occupied, blocked_locked, blocked_depth,
skipped_sender, skipped_self, failed
skipped_sender, skipped_self, skipped_manager, failed
"""
if not sender or not sender.strip():
logger.info("[monitor] Wake-back skipped — no sender")
Expand All @@ -119,8 +121,22 @@ def _wake_sender(sender: str, branch_email: str, exit_code: int, lock_file: str)
from aipass.ai_mail.apps.handlers.dispatch.wake import wake_branch

os.environ["AIPASS_WAKE_DEPTH"] = str(depth + 1)
# sender="" terminates the wake-back chain. It also means the @daemon
# exception inside the manager gate can never apply here — a wake-back is
# never a daemon-scheduled self-wake — so managers always hit the skip path.
wake_status, success = wake_branch(sender, auto=True, sender="")

# Must precede the success check: the manager gate returns True having woken
# nothing, so trusting the bool alone logged "woken" for a wake that never
# happened. The status object was honest all along; read it instead.
manager_step = wake_status.find_step("manager")
if manager_step and manager_step[0] == "info":
logger.info(
"[monitor] Wake-back skipped — sender %s is citizen_class=manager (mail delivered, never woken)",
sender,
)
return "skipped_manager"

if success:
logger.info("[monitor] Wake-back: %s woken after %s completed (exit %d)", sender, branch_email, exit_code)
return "success"
Expand Down
32 changes: 26 additions & 6 deletions src/aipass/ai_mail/apps/handlers/dispatch/wake.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# =================== AIPass ====================
# Name: wake.py
# Description: Manual Branch Wake Handler
# Version: 2.0.1
# Version: 2.1.0
# Created: 2026-03-02
# Modified: 2026-07-31
# Modified: 2026-08-04
# =============================================

"""
Expand Down Expand Up @@ -115,6 +115,18 @@ def format(self) -> str:
lines.append(f"{icon} {label} → {detail}")
return "\n".join(lines)

def find_step(self, label: str) -> Optional[Tuple[str, str, str]]:
"""Return the last (status, label, detail) recorded under `label`, or None.

Lets callers read a specific gate's own verdict instead of pattern-matching
the prose in `summary`. Needed because the overall success bool cannot express
"delivered, but deliberately not woken" — see the manager gate in wake_branch.
"""
for step in reversed(self.steps):
if step[1] == label:
return step
return None

@property
def summary(self) -> str:
"""Single-line summary from last step."""
Expand Down Expand Up @@ -577,6 +589,13 @@ def wake_branch(

Returns:
Tuple of (DispatchStatus with all steps, overall success bool)

The bool means "the dispatch did what it should", NOT "an agent was woken".
A manager target returns True having deliberately woken nothing — mail is
delivered and the wake is skipped by design (see Step 3). Callers that need
to know whether a process actually started must check
status.find_step("manager"): "info" = gate skipped the wake, "ok" = the
@daemon self-wake exception applied and the spawn went ahead.
"""
json_handler.log_operation(
"wake_branch", {"branch": branch_email, "fresh": fresh, "auto": auto, "model": model or DEFAULT_MODEL}
Expand Down Expand Up @@ -670,10 +689,11 @@ def wake_branch(
prompt = f"Hi. {custom_message} "
else:
prompt = f"{DEFAULT_PROMPT} "
# Monitor owns lock cleanup end-to-end — telling the agent to delete it
# too let a second monitor spawn onto a "clear" lock while the first was
# still alive, then have its own unconditional cleanup steal the second
# monitor's lock out from under it (lock-theft, observed 2026-07-31).
# Monitor owns lock cleanup end-to-end, so the prompt no longer tells the
# agent to delete the lock: an agent deleting it while its own monitor is
# still alive lets a second monitor spawn onto a "clear" lock, and the two
# then race over one lock file. Rationale from reading the cleanup paths —
# not a logged incident; the monitor's PID-verified cleanup is the guard.
prompt += (
"IMPORTANT: run any sub-agents synchronously (foreground) and wait for them to "
"finish before ending your turn — headless dispatch kills orphaned background "
Expand Down
Loading