feat(issue-5): report finalization manifest and side-effect evidence
The agent declares that its work is finished; the reader re-derives the hash from the file rather than trusting the number in the declaration. A manifest the watcher wrote itself would attest only that the watcher saw a stable file, which is the self-certifying evidence shape rejected earlier in this issue. Finalizing promotes a staged report by atomic rename before writing the manifest, so a reader cannot observe a half-written report as final. Rewriting the report afterwards invalidates the declaration immediately, because the recomputed hash no longer matches -- a new body needs a new finalization. A job that declared side effects must show evidence for every one of them. Partial evidence is treated as none: the work is not known to be complete. publish_event gains --finalize-report, --side-effect and --attempt-id, and does the finalization before publishing. A `completed` event whose report is still being written would invite exactly the partial adoption the contract forbids, so a failure there fails the publish. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
9a827b3caa
commit
3ea5757cfe
3 files changed
+327
No files matched your search
@@ -205,6 +205,15 @@ def main(argv=None) -> int:
|
||||
help="force retain=True (auto for completed/error)")
|
||||
parser.add_argument("--registry-dir", default=DEFAULT_REGISTRY_DIR)
|
||||
parser.add_argument("--attempts", type=int, default=3)
|
||||
parser.add_argument("--finalize-report", default=None, metavar="PATH",
|
||||
help="atomically finalize this report and declare its "
|
||||
"completion manifest before publishing")
|
||||
parser.add_argument("--side-effect", action="append", default=[], dest="side_effects",
|
||||
metavar="KIND=REFERENCE",
|
||||
help="evidence for an external side effect this job performed")
|
||||
parser.add_argument("--attempt-id", type=int, default=None,
|
||||
help="attempt this finalization belongs to (default: the "
|
||||
"job's current iteration)")
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
@@ -227,6 +236,33 @@ def main(argv=None) -> int:
|
||||
logger.error("no job to publish for (unknown --job or no pending job)")
|
||||
return 1
|
||||
|
||||
# Finalize before publishing: a `completed` event whose report is still being
|
||||
# written would invite exactly the partial-report adoption the contract
|
||||
# forbids. Failing here fails the publish, on purpose.
|
||||
if args.finalize_report:
|
||||
from lib_py.loop import finalize # local import: optional dependency of this CLI
|
||||
|
||||
try:
|
||||
side_effects = finalize.parse_side_effects(args.side_effects)
|
||||
except ValueError as exc:
|
||||
logger.error("%s", exc)
|
||||
return 1
|
||||
attempt_id = args.attempt_id
|
||||
if attempt_id is None:
|
||||
try:
|
||||
attempt_id = load_job(job_id, args.registry_dir).get("iteration", 1)
|
||||
except FileNotFoundError:
|
||||
attempt_id = 1
|
||||
try:
|
||||
manifest = finalize.finalize_report(
|
||||
args.finalize_report, job_id, attempt_id,
|
||||
side_effects=side_effects, declared_by="agent")
|
||||
except (OSError, FileNotFoundError) as exc:
|
||||
logger.error("could not finalize report: %s", exc)
|
||||
return 1
|
||||
logger.info("finalized %s (sha256 %s...)", manifest["report_path"],
|
||||
manifest["sha256"][:12])
|
||||
|
||||
try:
|
||||
prepared = prepare_event(
|
||||
job_id=job_id,
|
||||
|
||||
Reference in new issue
Block a user