feat(issue-5): auto-completion gate and durable pending notification

The gate does not re-decide anything. Phase 2a already decides, fail-closed and
against contract-pinned vectors; this supplies real evidence to that decision and
commits only if three independent gates agree: the global switch is on, the
backend has observed evidence that it reaches a quiescent state, and the job
itself opted in. Default is off.

The backend allowlist is evidence-based rather than optimistic. Inferring
"finished" from stillness requires stillness to have been observed, and only
three of six backends have that. An operator may narrow the list; a name that
was never observed is dropped rather than trusted, so it cannot be widened by
configuration.

Everything else holds. An unreadable manifest, a report rewritten after
finalization, missing evidence for a declared side effect, a policy that raises:
all refuse. "Could not decide" is never "done". A late explicit terminal wins,
because the boundary refuses the inferred one -- that is the boundary working,
not an error in the gate.

Notification intent is committed in the same atomic write as the state, so a
crash between committing and publishing leaves a record that still says the
world has not been told. Recovery re-sends; duplicates are already dropped by
sequence. A failed send deliberately leaves the intent pending, because an
unsent notification that looks sent is worse than one that is retried.

Two fixes this surfaced. Settling used a request that terminal records refuse,
which made an owed notification impossible to ever settle; settling is now a
status-free bookkeeping request, valid in any state. And the phase 2a guard
against shell references was matching hashlib.sha256 as a substring, so it now
requires a word boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-06 21:30:38 +09:00
1 parent 3ea5757cfe
commit 803ba43884
5 files changed
+513 -4

No files matched your search

+11 -2
View File
@@ -691,10 +691,19 @@ def test_g9_loop_package_imports_only_delegation_among_siblings():
def test_g9_phase2a_spawns_no_daemon_and_calls_no_frozen_helper():
"""The two phase 1 handoffs stay dormant: preexec_fn is unreachable because
nothing here spawns, and no frozen lib.sh helper is invoked."""
import re
# `lib.sh` needs a word boundary: a bare substring search also matches
# hashlib.sha256, which is how this guard first fired on innocent code.
patterns = {
"ensure_subscriber_daemon": re.compile(r"ensure_subscriber_daemon"),
"preexec_fn": re.compile(r"preexec_fn"),
"lib.sh": re.compile(r"\blib\.sh\b"),
}
for path in sorted((SKILLS / "lib_py" / "loop").glob("*.py")):
src = path.read_text(encoding="utf-8")
for forbidden in ("ensure_subscriber_daemon", "preexec_fn", "lib.sh"):
assert forbidden not in src, "{} references {}".format(path.name, forbidden)
for name, pattern in patterns.items():
assert not pattern.search(src), "{} references {}".format(path.name, name)
# ==============================================================================