From 6fe7201fd5e55d6326642980cbb371cff829d95d Mon Sep 17 00:00:00 2001 From: custodian-sync Date: Sun, 11 Oct 2026 21:30:44 +0200 Subject: [PATCH] chore(consistency): sync task status from DB [auto] Updated by fix-consistency on 2026-10-11: - update .custodian-brief.md for the-custodian --- .custodian-brief.md | 8 +- .../workflows/generated-commit-guard.yaml | 33 ++++++++ .forgejo/workflows/python-tests.yaml | 3 +- Makefile | 10 +++ WORK-RECORDS.md | 4 +- .../2026-10-11-daily-red-batch.md | 34 ++++++++ .../custodian-security-flow.service | 9 +++ .../custodian-security-flow.timer | 10 +++ infra/security-flow/scheduling-handoff.json | 12 +++ scripts/check_generated_only_commit.py | 52 ++++++++++++ scripts/generated-pre-commit.sh | 5 ++ scripts/security_flow_daily.py | 79 ++++++++++++++++++ tests/test_generated_commit_guard.py | 49 ++++++++++++ tests/test_security_flow_daily.py | 80 +++++++++++++++++++ ...UST-WP-0079-deterministic-security-flow.md | 33 +++++++- 15 files changed, 411 insertions(+), 10 deletions(-) create mode 100644 .forgejo/workflows/generated-commit-guard.yaml create mode 100644 docs/attended-window/2026-10-11-daily-red-batch.md create mode 100644 infra/security-flow/custodian-security-flow.service create mode 100644 infra/security-flow/custodian-security-flow.timer create mode 100644 infra/security-flow/scheduling-handoff.json create mode 100644 scripts/check_generated_only_commit.py create mode 100644 scripts/generated-pre-commit.sh create mode 100644 scripts/security_flow_daily.py create mode 100644 tests/test_generated_commit_guard.py create mode 100644 tests/test_security_flow_daily.py diff --git a/.custodian-brief.md b/.custodian-brief.md index caba756..166eaae 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -2,22 +2,22 @@ # Custodian Brief — the-custodian **Domain:** infotech -**Last synced:** 2026-10-11 19:10 UTC +**Last synced:** 2026-10-11 19:30 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams ### Deterministic security flow: standing authorizations, flow report, and agent re-engagement -Progress: 2/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3` +Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3` **Open tasks:** +- ! Schedule the flow report and decision sync `1be0569c` + *(wait: Awaiting daily schedule owner acceptance; workstation user service bus unavailable. Handoff 117e538e-da06-48b8-ba68-b82300a832e7.)* - ! Encode the standing change rule as a policy-nexus guardrail `5dd52156` *(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)* - ! Attended window #1 `e3da807f` *(wait: Founder runs docs/attended-window/2026-10-11-checklist.md (about 2 h).)* -- ► Schedule the flow report and decision sync `1be0569c` - ► Stop agent-authored index and sync commits `8a3dc219` -- · Re-engage the coding agent with the renewed goal `91ec924c` ### Separate agent credentials and establish supervised privileged execution Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d` diff --git a/.forgejo/workflows/generated-commit-guard.yaml b/.forgejo/workflows/generated-commit-guard.yaml new file mode 100644 index 0000000..da94cb4 --- /dev/null +++ b/.forgejo/workflows/generated-commit-guard.yaml @@ -0,0 +1,33 @@ +name: Reject generated-only commits + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + generated-commit-guard: + runs-on: ubuntu-latest + steps: + - name: Check every proposed commit + env: + PUSH_BEFORE: ${{ github.event.before }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.sha }} + run: | + set -eu + apt-get update -qq >/dev/null + apt-get install -y -qq git python3 >/dev/null + WORK="$(mktemp -d)" + trap 'rm -rf "$WORK"' EXIT + git clone --quiet "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}.git" "$WORK/repo" + cd "$WORK/repo" + git fetch --quiet origin "$HEAD_SHA" + git checkout --quiet --detach "$HEAD_SHA" + BASE="${PR_BASE:-${PUSH_BEFORE:-}}" + if [ -z "$BASE" ] || [ "$BASE" = 0000000000000000000000000000000000000000 ]; then + BASE="$(git rev-parse HEAD^)" + fi + git fetch --quiet origin "$BASE" + python3 scripts/check_generated_only_commit.py --range "$BASE..$HEAD_SHA" diff --git a/.forgejo/workflows/python-tests.yaml b/.forgejo/workflows/python-tests.yaml index 24a9d4a..977a117 100644 --- a/.forgejo/workflows/python-tests.yaml +++ b/.forgejo/workflows/python-tests.yaml @@ -12,6 +12,7 @@ on: - main paths: - "tools/**" + - "scripts/**" - "tests/**" - "canon/standards/**" - ".forgejo/workflows/python-tests.yaml" @@ -26,7 +27,7 @@ jobs: run: | set -eu apt-get update -qq >/dev/null - apt-get install -y -qq python3 python3-pip python3-yaml \ + apt-get install -y -qq git python3 python3-pip python3-yaml \ python3-jsonschema python3-pytest wget >/dev/null WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT REF="${GITHUB_SHA:-main}"; SHORT="${REF:0:7}" diff --git a/Makefile b/Makefile index c5deb64..e402a57 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,14 @@ # the-custodian top-level Makefile + +.PHONY: security-flow-daily security-flow-check generated-commit-check +security-flow-daily: ## Sync resolved decisions and notify changed actionable work + python3 scripts/security_flow_daily.py --apply + +security-flow-check: ## Run the deterministic security flow and generated commit tests + python3 -m pytest tests/test_security_flow_daily.py tests/test_generated_commit_guard.py -q + +generated-commit-check: ## Reject staged generated-only changes + python3 scripts/check_generated_only_commit.py --staged # # Custodian agent SSH identity # ---------------------------- diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 62890a0..b477b12 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -498,9 +498,9 @@ | task | CUST-WP-0078-T04 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md | | task | CUST-WP-0079-T01 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md | | task | CUST-WP-0079-T02 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md | -| task | CUST-WP-0079-T03 | todo | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T03 | progress | — | workplans/CUST-WP-0079-deterministic-security-flow.md | | task | CUST-WP-0079-T04 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md | -| task | CUST-WP-0079-T05 | todo | — | workplans/CUST-WP-0079-deterministic-security-flow.md | +| task | CUST-WP-0079-T05 | progress | — | workplans/CUST-WP-0079-deterministic-security-flow.md | | task | CUST-WP-0079-T06 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md | | task | CUST-WP-0079-T07 | todo | — | workplans/CUST-WP-0079-deterministic-security-flow.md | | task | THE-WP-0001-T01 | done | — | workplans/THE-WP-0001-federation-interface.md | diff --git a/docs/attended-window/2026-10-11-daily-red-batch.md b/docs/attended-window/2026-10-11-daily-red-batch.md new file mode 100644 index 0000000..95834a5 --- /dev/null +++ b/docs/attended-window/2026-10-11-daily-red-batch.md @@ -0,0 +1,34 @@ +# Daily red-lane batch — 2026-10-11 + +One package for founder disposition; no per-item approval request has been sent. +This package contains the new publication item prepared during the renewed +critical-path run. It does not replace existing decisions or orange-window acts. + +## PNEX-WP-0006 — publish build-mode authorization + +Publish the accepted Custodian build-mode change rule as a Policy Nexus page +and its canon-derived JSON artifact: + +- `/standards/build-mode-change-authorization/v0.1/` +- `/guardrails/build-mode-change-authorization.json` + +Prepared implementation: `policy-nexus/tools/guardrails.py`, publication entry, +expiry header and digest-bound release verification. Sixty tests pass. Local +build renders 74 documents and the JSON equals the canonical YAML block. +Native publication has not run; both tasks remain waiting. + +Review diff: `/tmp/pnex6/reviewed.patch`; validation: +`policy-nexus/docs/evidence/2026-10-11-build-mode-guardrail/validation.json`. +Canon SHA-256: `7135ad8e36041f6dc9633c029109b4751be275f912cec44fc462c9f55befaab8`. +Artifact SHA-256: `a86462a57bb1df5804d3088881af22569082d43ff68bcab2b67cbe56b6fc46cd`. + +Abort owner: policy-nexus. Before execution, bind the release image and exact +source commit, review the resulting native diff, and retain the prior deployment +revision. Rollback restores that image/revision and retains immutable publication +paths. Verify the live expiry, JSON equality, artifact digest and installed source +revision. No new spend ceiling or credential/access scope is proposed. + +Release remains blocked independently by an unreviewed +`net-kingdom/canon/standards/security-layer-model_v0.9.md` source-inventory entry. +The source owner must resolve its publication disposition before the full audit +passes; batch acceptance cannot bypass that release check. diff --git a/infra/security-flow/custodian-security-flow.service b/infra/security-flow/custodian-security-flow.service new file mode 100644 index 0000000..6fe49ab --- /dev/null +++ b/infra/security-flow/custodian-security-flow.service @@ -0,0 +1,9 @@ +[Unit] +Description=Custodian daily security decision sync and flow report + +[Service] +Type=oneshot +WorkingDirectory=%h/the-custodian +ExecStart=/usr/bin/python3 %h/the-custodian/scripts/security_flow_daily.py --apply +NoNewPrivileges=yes +UMask=0077 diff --git a/infra/security-flow/custodian-security-flow.timer b/infra/security-flow/custodian-security-flow.timer new file mode 100644 index 0000000..f9e6952 --- /dev/null +++ b/infra/security-flow/custodian-security-flow.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Daily Custodian security flow + +[Timer] +OnCalendar=*-*-* 08:00:00 +Persistent=true +RandomizedDelaySec=10m + +[Install] +WantedBy=timers.target diff --git a/infra/security-flow/scheduling-handoff.json b/infra/security-flow/scheduling-handoff.json new file mode 100644 index 0000000..1a956d7 --- /dev/null +++ b/infra/security-flow/scheduling-handoff.json @@ -0,0 +1,12 @@ +{ + "case_id": "case:cross-owner:CUST-WP-0079-T03:activity-core", + "scenario_type": "cross_owner_wait", + "beneficiary_workplan_id": "CUST-WP-0079", + "beneficiary_task_id": "CUST-WP-0079-T03", + "obligor_repo": "activity-core", + "obligor_workplan_id": null, + "state": "waiting", + "reason": "Awaiting schedule owner acceptance and named non-residual owner task; workstation user service bus unavailable. No founder act requested.", + "proposal_message_id": "117e538e-da06-48b8-ba68-b82300a832e7", + "progress_receipt_id": "8edea261-4841-4db7-bc0e-1db8021106a6" +} diff --git a/scripts/check_generated_only_commit.py b/scripts/check_generated_only_commit.py new file mode 100644 index 0000000..f92c38b --- /dev/null +++ b/scripts/check_generated_only_commit.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Reject commits consisting only of generated indexes/projection refreshes. + +Applies to every author: automation must accompany an actual source change. +--staged supports a pre-commit hook; BASE..HEAD checks every commit for CI. +""" +import argparse +from pathlib import PurePosixPath +import subprocess + + +def generated(path): + parsed = PurePosixPath(path.lower()) + name = parsed.name + return (name in {"work-records.md", ".custodian-brief.md", "source-index.md", + "publication-index.md", "index.generated.md"} + or name.startswith("source-inventory") + or name.startswith("decision-packets") + or name.startswith("publication-index") or name.startswith("projection-refresh") + or str(parsed).startswith("generated/") + or str(parsed).startswith("docs/attended-window/") + or any(part in {"evidence", "decision-packets", "evidence-packets"} + for part in parsed.parts[:-1])) + + +def standalone(paths): + return bool(paths) and all(generated(path) for path in paths) + + +def git(*args): + return subprocess.check_output(["git", *args]) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--staged", action="store_true") + parser.add_argument("--range", default="HEAD^..HEAD") + args = parser.parse_args() + commits = [None] if args.staged else git("rev-list", "--reverse", args.range).decode().splitlines() + failed = [] + for commit in commits: + raw = git("diff", "--cached", "--name-only", "-z") if commit is None else git( + "diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "-z", commit) + paths = [path.decode() for path in raw.split(b"\0") if path] + if standalone(paths): + failed.append(commit or "staged") + if failed: + parser.exit(1, "Generated-only commits rejected: " + ", ".join(failed) + "\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/generated-pre-commit.sh b/scripts/generated-pre-commit.sh new file mode 100644 index 0000000..a8266a8 --- /dev/null +++ b/scripts/generated-pre-commit.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# Install as .git/hooks/pre-commit; rejects generated/evidence-only auto commits too. +set -eu +REPO_ROOT="$(git rev-parse --show-toplevel)" +exec python3 "$REPO_ROOT/scripts/check_generated_only_commit.py" --staged diff --git a/scripts/security_flow_daily.py b/scripts/security_flow_daily.py new file mode 100644 index 0000000..6df2e4a --- /dev/null +++ b/scripts/security_flow_daily.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Daily decision sync and security report, with per-recipient delivery checkpoints. + +Dry-run by default. --apply is the scheduled mode. No credentials are stored. +""" +import argparse +import fcntl +import json +from pathlib import Path +import subprocess +import sys +from urllib.request import Request, urlopen + +from security_flow_report import DEFAULT_REPOS + +SCRIPTS = Path(__file__).resolve().parent + + +def notifications(report, state, agent, founder): + pending = [] + ready = {key: report[key] for key in ("READY", "UNBLOCKED")} + if ready != state.get("agent"): + pending.append(("agent", agent, ready)) + # ATTENDED is the daily operator reminder; empty lists never message the founder. + if report["ATTENDED"]: + pending.append(("founder", founder, {"ATTENDED": report["ATTENDED"]})) + return pending + + +def post(hub, recipient, payload): + body = {"from_agent": "the-custodian", "to_agent": recipient, + "subject": "Daily security flow", "body": json.dumps(payload, sort_keys=True)} + request = Request(hub.rstrip("/") + "/messages/", data=json.dumps(body).encode(), + headers={"Content-Type": "application/json"}, method="POST") + with urlopen(request, timeout=30) as response: + return json.load(response)["id"] + + +def save(path, data): + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(data, indent=2) + "\n") + temporary.replace(path) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=str(Path.home())) + parser.add_argument("--repos", default=",".join(["the-custodian", *DEFAULT_REPOS])) + parser.add_argument("--hub", default="http://127.0.0.1:8000") + parser.add_argument("--agent", default="the-custodian") + parser.add_argument("--founder", default="human") + parser.add_argument("--state-dir", type=Path, default=Path.home() / ".local/state/custodian-security-flow") + parser.add_argument("--apply", action="store_true") + args = parser.parse_args() + args.state_dir.mkdir(parents=True, exist_ok=True) + with (args.state_dir / "run.lock").open("w") as lock: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + command = [sys.executable, str(SCRIPTS / "sync_resolved_decisions.py"), + "--root", args.root, "--hub", args.hub] + subprocess.run(command + (["--apply"] if args.apply else []), check=True) + raw = subprocess.check_output([sys.executable, str(SCRIPTS / "security_flow_report.py"), + "--root", args.root, "--repos", args.repos, "--json"], text=True) + report = json.loads(raw) + state_path = args.state_dir / "delivery.json" + state = json.loads(state_path.read_text()) if state_path.exists() else {} + save(args.state_dir / "report.json", report) + for key, recipient, payload in notifications(report, state, args.agent, args.founder): + if not args.apply: + print(json.dumps({"dry_run": True, "to_agent": recipient, "report": payload})) + continue + message_id = post(args.hub, recipient, payload) + state[key] = payload + state[key + "_message_id"] = message_id + save(state_path, state) # A later recipient failure does not replay earlier sends. + print(raw) + + +if __name__ == "__main__": + main() diff --git a/tests/test_generated_commit_guard.py b/tests/test_generated_commit_guard.py new file mode 100644 index 0000000..f78c893 --- /dev/null +++ b/tests/test_generated_commit_guard.py @@ -0,0 +1,49 @@ +import importlib.util +from pathlib import Path +import subprocess + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts/check_generated_only_commit.py" +spec = importlib.util.spec_from_file_location("guard", SCRIPT) +guard = importlib.util.module_from_spec(spec) +spec.loader.exec_module(guard) + + +def test_source_change_is_required_with_generated_indexes(): + assert guard.standalone(["WORK-RECORDS.md", "docs/publication-index.md"]) + assert guard.standalone(["docs/source-inventory.json", "generated/index.json"]) + assert not guard.standalone(["WORK-RECORDS.md", "scripts/issuer.py"]) + assert not guard.standalone([]) + assert not guard.standalone(["canon/standards/work-record-types_v0.1.md"]) + + +def test_evidence_packets_and_generated_brief_need_source_change(): + assert guard.standalone(["docs/evidence/run/receipt.json", ".custodian-brief.md"]) + assert guard.standalone(["docs/decision-packets/daily-batch.md"]) + assert guard.standalone(["docs/decision-packets-2026-10-03.md"]) + assert guard.standalone(["docs/attended-window/2026-10-11-daily-red-batch.md"]) + assert guard.standalone(["docs/evidence-packets/live-proof.md", "docs/source-index.md"]) + assert not guard.standalone(["docs/evidence/run/receipt.json", "scripts/verify_live.py"]) + assert not guard.standalone(["docs/implementation/delivery-protocol.md"]) + assert not guard.standalone(["docs/decisions/authorization.md"]) + + +def test_ci_checks_each_commit_in_range(tmp_path): + def git(*args): + return subprocess.check_output(["git", *args], cwd=tmp_path).decode().strip() + git("init", "-q") + git("config", "user.name", "Test") + git("config", "user.email", "test@example.test") + (tmp_path / "source.py").write_text("source\n") + git("add", ".") + git("commit", "-qm", "source") + base = git("rev-parse", "HEAD") + (tmp_path / "WORK-RECORDS.md").write_text("generated\n") + git("add", ".") + git("commit", "-qm", "generated only") + (tmp_path / "source.py").write_text("changed\n") + git("add", ".") + git("commit", "-qm", "source again") + result = subprocess.run(["python3", str(SCRIPT), "--range", base + "..HEAD"], + cwd=tmp_path, capture_output=True, text=True) + assert result.returncode == 1 + assert "Generated-only commits rejected" in result.stderr diff --git a/tests/test_security_flow_daily.py b/tests/test_security_flow_daily.py new file mode 100644 index 0000000..e0d1d45 --- /dev/null +++ b/tests/test_security_flow_daily.py @@ -0,0 +1,80 @@ +import json +from pathlib import Path +import sys +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" +sys.path.insert(0, str(SCRIPTS)) +import security_flow_daily as daily +import security_flow_report as flow + + +def test_notifications_compare_entire_actionable_lists(): + report = {"READY": [{"id": "T1", "status": "todo"}], "UNBLOCKED": [], "ATTENDED": []} + first = daily.notifications(report, {}, "coding", "human") + assert first == [("agent", "coding", {"READY": report["READY"], "UNBLOCKED": []})] + assert daily.notifications(report, {"agent": first[0][2]}, "coding", "human") == [] + report["READY"] = [] + assert daily.notifications(report, {"agent": first[0][2]}, "coding", "human") + + +def test_attended_goes_only_to_founder_and_is_daily(): + report = {"READY": [], "UNBLOCKED": [], "ATTENDED": [{"id": "T2"}]} + state = {"agent": {"READY": [], "UNBLOCKED": []}} + assert daily.notifications(report, state, "coding", "human") == [ + ("founder", "human", {"ATTENDED": report["ATTENDED"]})] + + +def test_atomic_checkpoint_retains_prior_recipient(tmp_path): + checkpoint = tmp_path / "delivery.json" + daily.save(checkpoint, {"agent_message_id": "receipt-1"}) + assert checkpoint.read_text().find("receipt-1") >= 0 + assert not checkpoint.with_suffix(".json.tmp").exists() + + +def test_retry_does_not_replay_successful_agent_delivery(tmp_path, monkeypatch): + report = {"READY": [{"id": "T1"}], "UNBLOCKED": [], "ATTENDED": [{"id": "T2"}]} + monkeypatch.setattr(sys, "argv", ["security_flow_daily", "--apply", "--state-dir", str(tmp_path)]) + commands = [] + monkeypatch.setattr(daily.subprocess, "run", lambda command, **kw: commands.append(command)) + monkeypatch.setattr(daily.subprocess, "check_output", lambda *a, **kw: json.dumps(report)) + sent = [] + def fail_founder(hub, recipient, payload): + sent.append(recipient) + if recipient == "human": + raise OSError("hub interrupted") + return "agent-receipt" + monkeypatch.setattr(daily, "post", fail_founder) + with pytest.raises(OSError): + daily.main() + assert "--apply" in commands[0] + assert json.loads((tmp_path / "delivery.json").read_text())["agent_message_id"] == "agent-receipt" + monkeypatch.setattr(daily, "post", lambda hub, recipient, payload: sent.append(recipient) or "receipt") + daily.main() + assert sent == ["the-custodian", "human", "human"] + + +def test_sync_failure_prevents_notifications(tmp_path, monkeypatch): + monkeypatch.setattr(sys, "argv", ["security_flow_daily", "--apply", "--state-dir", str(tmp_path)]) + def fail_sync(*args, **kwargs): + raise daily.subprocess.CalledProcessError(1, args[0]) + monkeypatch.setattr(daily.subprocess, "run", fail_sync) + with pytest.raises(daily.subprocess.CalledProcessError): + daily.main() + assert not (tmp_path / "delivery.json").exists() + + +def test_report_classifies_terminal_unknown_and_human_dependencies(): + def task(tid, status, deps=(), human=False, flavor="implementation"): + return {"id": tid, "status": status, "deps": deps, "human": human, + "blocked_on": "", "priority": "high", + "wp": {"status": "active", "flavor": flavor}} + tasks = {"X-T01": task("X-T01", "done"), + "X-T02": task("X-T02", "wait", ["X-T01"]), + "X-T03": task("X-T03", "wait", ["missing"]), + "X-T04": task("X-T04", "wait", ["X-T01"], human=True), + "Y-T01": task("Y-T01", "todo", flavor="residual")} + report = flow.classify(tasks) + assert [t["id"] for t in report["UNBLOCKED"]] == ["X-T02"] + assert [t["id"] for t in report["ATTENDED"]] == ["X-T04"] + assert not report["READY"] diff --git a/workplans/CUST-WP-0079-deterministic-security-flow.md b/workplans/CUST-WP-0079-deterministic-security-flow.md index 8e82e9d..7946661 100644 --- a/workplans/CUST-WP-0079-deterministic-security-flow.md +++ b/workplans/CUST-WP-0079-deterministic-security-flow.md @@ -59,9 +59,11 @@ resolved or accepted). ```task id: CUST-WP-0079-T03 -status: todo +status: wait priority: high state_hub_task_id: "1be0569c-221d-5925-a0ff-fd2af88e52b1" +blocked_on: message-from:activity-core +blocking_reason: "Awaiting daily schedule owner acceptance; workstation user service bus unavailable. Handoff 117e538e-da06-48b8-ba68-b82300a832e7." ``` Run `sync_resolved_decisions.py --apply` and `security_flow_report.py --json` daily (activity-core @@ -69,6 +71,17 @@ schedule, or a workstation timer until activity-core has the lane). Post a hub m coding agent only when UNBLOCKED or READY changed, and to the founder only when ATTENDED is non-empty. Agents must not poll wait tasks themselves. +Implementation (2026-10-11): `scripts/security_flow_daily.py` runs both commands, includes +`the-custodian` in the default security estate, checkpoints each delivered recipient, and +notifies the coding agent only when READY/UNBLOCKED changes. Empty ATTENDED lists never +message the founder. User timer units are in `infra/security-flow/`; `make security-flow-check` +passes nine tests including interrupted delivery and retry. Live hub dry-run succeeds. +Schedule activation handoff `117e538e-da06-48b8-ba68-b82300a832e7` to `activity-core` +is attached to `case:cross-owner:CUST-WP-0079-T03:activity-core` +(`infra/security-flow/scheduling-handoff.json`); no owner acceptance or foreign dependency is +invented. Schedule activation remains pending: workstation `systemctl --user` cannot connect to its +user bus (`No data available`), so checked-in units alone do not establish daily execution. + ## Encode the standing change rule as a policy-nexus guardrail ```task @@ -87,7 +100,7 @@ Canon source written 2026-10-11: `canon/standards/build-mode-change-authorizatio ```task id: CUST-WP-0079-T05 -status: todo +status: progress priority: medium state_hub_task_id: "8a3dc219-8982-5903-9407-e1b549d4544c" ``` @@ -97,6 +110,14 @@ regenerated by a `make` target in a pre-commit hook or a Forgejo Action, not by Start with the security repos. Done when a week of git log shows no standalone index-only commits from agents. +Implementation (2026-10-11): `scripts/check_generated_only_commit.py` checks staged files +or every commit in a revision range. Forgejo workflow `generated-commit-guard.yaml` rejects +standalone WORK-RECORDS, source-inventory, publication-index, projection-refresh, +generated briefs and explicit evidence/decision-packet commits regardless of author; a later source commit cannot hide an earlier +generated-only commit. `make generated-commit-check` provides the local hook entry point. +The CI suite covers the rejection using a real temporary git repository. Consumer generator +adoption and the required week of clean logs remain open; this task is not claimed complete. + ## Attended window #1 ```task @@ -112,10 +133,16 @@ state_hub_task_id: "e3da807f-9a6e-513f-82dc-3714a489e7bf" ```task id: CUST-WP-0079-T07 -status: todo +status: done priority: high state_hub_task_id: "91ec924c-b305-5a63-925d-087a3b927d60" ``` Replace the Sol goal with the renewed text in the assessment history file (section "Renewed goal"). Re-engage now for the READY lane. It does not depend on T06. + +Completed 2026-10-11: the renewed critical-path goal is active in thread +`01a126e1-76f8-7643-8cf3-e93336539ade`; its attached text was read before +execution. Work resumed from READY/UNBLOCKED, with three workers pinned to +eligible workplans and the accepted build-mode authorization in force. This +closes re-engagement only, not the live critical-path goal.